From f6d44838440ecb0b43b7d757906272aa5e52bfe6 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Wed, 21 Jun 2023 15:25:10 -0700 Subject: [PATCH 01/11] add DateTime and DateTimeOffset types to the system type to json type converter used in OpenAPI doc creation. --- src/Core/Services/TypeHelper.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 4536878f8e..1e922eaad9 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -96,7 +96,9 @@ public static class TypeHelper [typeof(bool?)] = JsonDataType.Boolean, [typeof(char?)] = JsonDataType.String, [typeof(Guid?)] = JsonDataType.String, - [typeof(object)] = JsonDataType.Object + [typeof(object)] = JsonDataType.Object, + [typeof(DateTime)] = JsonDataType.String, + [typeof(DateTimeOffset)] = JsonDataType.String }; /// @@ -113,6 +115,10 @@ public static string SystemTypeToJsonDataType(Type type) { openApiJsonTypeName = JsonDataType.Undefined; } + else + { + Console.Out.WriteLine("unknown type"); + } string formattedOpenApiTypeName = openApiJsonTypeName.ToString().ToLower(); return formattedOpenApiTypeName; From 070e4198be9c1e93a53636fca863e8036e2739cf Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 23 Jun 2023 16:12:13 -0700 Subject: [PATCH 02/11] Update JsonValueType handling for OpenApiDocumentor, and improved sql db type mapping to CLR(system) and JsonDataType/DbType. And added tests. --- src/Core/Models/SqlTypeConstants.cs | 55 ++++++++ .../MsSqlMetadataProvider.cs | 51 +------ .../MetadataProviders/SqlMetadataProvider.cs | 1 + .../Services/OpenAPI/OpenApiDocumentor.cs | 6 +- src/Core/Services/TypeHelper.cs | 128 ++++++++++++++---- .../Configuration/ConfigurationTests.cs | 2 +- .../CLRtoJsonValueTypeUnitTests.cs | 56 ++++++++ 7 files changed, 222 insertions(+), 77 deletions(-) create mode 100644 src/Core/Models/SqlTypeConstants.cs create mode 100644 src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs new file mode 100644 index 0000000000..276578a147 --- /dev/null +++ b/src/Core/Models/SqlTypeConstants.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Azure.DataApiBuilder.Service.Models +{ + /// + /// String literal representation of SQL Server data types that are returned by Microsoft.Data.SqlClient. + /// + /// + public class SqlTypeConstants + { + /// + /// SqlDbType names ordered by corresponding SqlDbType. + /// Keys are lower case to match formatting of SQL Server INFORMATION_SCHEMA DATA_TYPE column value. + /// Sourced directly from internal/private SmiMetaData.cs in Microsoft.Data.SqlClient + /// Value indicates whether DAB engine supports the SqlDbType. + /// + /// + public static readonly Dictionary SupportedSqlDbTypes = new() + { + { "bigint", true }, // SqlDbType.BigInt + { "binary", true }, // SqlDbType.Binary + { "bit", true }, // SqlDbType.Bit + { "char", true }, // SqlDbType.Char + { "datetime", true }, // SqlDbType.DateTime + { "decimal", true }, // SqlDbType.Decimal + { "float", true }, // SqlDbType.Float + { "image", true }, // SqlDbType.Image + { "int", true }, // SqlDbType.Int + { "money", true }, // SqlDbType.Money + { "nchar", true }, // SqlDbType.NChar + { "ntext", true }, // SqlDbType.NText + { "nvarchar", true }, // SqlDbType.NVarChar + { "real", true }, // SqlDbType.Real + { "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier + { "smalldatetime", true }, // SqlDbType.SmallDateTime + { "smallint", true }, // SqlDbType.SmallInt + { "smallmoney", true }, // SqlDbType.SmallMoney + { "text", true }, // SqlDbType.Text + { "timestamp", true }, // SqlDbType.Timestamp + { "tinyint", true }, // SqlDbType.TinyInt + { "varbinary", true }, // SqlDbType.VarBinary + { "varchar", true }, // SqlDbType.VarChar + { "sql_variant", false }, // SqlDbType.Variant (unsupported) + { "xml", false }, // SqlDbType.Xml (unsupported) + { "date", true }, // SqlDbType.Date + { "time", true }, // SqlDbType.Time + { "datetime2", true }, // SqlDbType.DateTime2 + { "datetimeoffset", true }, // SqlDbType.DateTimeOffset + { "", false }, // SqlDbType.Udt and SqlDbType.Structured (unsupported) + }; + } +} diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index 4c3619e1ed..e5a0ff1899 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -34,57 +34,12 @@ public override string GetDefaultSchemaName() } /// - /// Takes a string version of an MS SQL data type and returns its .NET common language runtime (CLR) counterpart - /// As per https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings + /// Takes a string version of an SQL Server data type and returns its .NET common language runtime (CLR) counterpart + /// As per https://docs.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings /// public override Type SqlToCLRType(string sqlType) { - switch (sqlType) - { - case "bigint": - case "numeric": - return typeof(decimal); - case "bit": - return typeof(bool); - case "smallint": - return typeof(short); - case "real": - case "decimal": - case "smallmoney": - case "money": - return typeof(decimal); - case "int": - return typeof(int); - case "tinyint": - return typeof(byte); - case "float": - return typeof(float); - case "date": - case "datetime2": - case "smalldatetime": - case "datetime": - case "time": - return typeof(DateTime); - case "datetimeoffset": - return typeof(DateTimeOffset); - case "char": - case "varchar": - case "text": - case "nchar": - case "nvarchar": - case "ntext": - return typeof(string); - case "binary": - case "varbinary": - case "image": - return typeof(byte[]); - case "uniqueidentifier": - return typeof(Guid); - default: - throw new DataApiBuilderException(message: $"Tried to convert unsupported data type: {sqlType}", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } + return TypeHelper.GetSystemTypeFromSqlDbType(sqlType); } } } diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs index 9f59d5ed79..57e72b6af5 100644 --- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs @@ -305,6 +305,7 @@ private async Task FillSchemaForStoredProcedureAsync( foreach (DataRow row in parameterMetadata.Rows) { // row["DATA_TYPE"] has value type string so a direct cast to System.Type is not supported. + // See https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings Type systemType = SqlToCLRType((string)row["DATA_TYPE"]); // Add to parameters dictionary without the leading @ sign storedProcedureDefinition.Parameters.TryAdd(((string)row["PARAMETER_NAME"])[1..], diff --git a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs index 86d9bfbab5..28a357afa6 100644 --- a/src/Core/Services/OpenAPI/OpenApiDocumentor.cs +++ b/src/Core/Services/OpenAPI/OpenApiDocumentor.cs @@ -553,7 +553,7 @@ private Tuple> CreatePrimaryKeyPathComponentAndPa { OpenApiSchema parameterSchema = new() { - Type = columnDef is not null ? TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType) : string.Empty + Type = columnDef is not null ? TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower() : string.Empty }; OpenApiParameter openApiParameter = new() @@ -855,9 +855,9 @@ private OpenApiSchema CreateComponentSchema(string entityName, HashSet f { string typeMetadata = string.Empty; string formatMetadata = string.Empty; - if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef) && columnDef is not null) + if (dbObject.SourceDefinition.Columns.TryGetValue(backingColumnValue, out ColumnDefinition? columnDef)) { - typeMetadata = TypeHelper.SystemTypeToJsonDataType(columnDef.SystemType).ToString().ToLower(); + typeMetadata = TypeHelper.GetJsonDataTypeFromSystemType(columnDef.SystemType).ToString().ToLower(); } properties.Add(field, new OpenApiSchema() diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 1e922eaad9..3173346a2a 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -7,10 +7,14 @@ namespace Azure.DataApiBuilder.Core.Services { /// - /// Helper class used to resolve CLR Type to the associated DbType or JsonDataType + /// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types. /// + /// public static class TypeHelper { + /// + /// Maps .NET Framework types to DbType enum + /// private static Dictionary _systemTypeToDbTypeMap = new() { [typeof(byte)] = DbType.Byte, @@ -29,6 +33,9 @@ public static class TypeHelper [typeof(char)] = DbType.StringFixedLength, [typeof(Guid)] = DbType.Guid, [typeof(byte[])] = DbType.Binary, + [typeof(DateTime)] = DbType.DateTime, + [typeof(DateTimeOffset)] = DbType.DateTimeOffset, + [typeof(TimeSpan)] = DbType.Time, [typeof(byte?)] = DbType.Byte, [typeof(sbyte?)] = DbType.SByte, [typeof(short?)] = DbType.Int16, @@ -47,22 +54,7 @@ public static class TypeHelper }; /// - /// Returns the DbType for given system type. - /// - /// The system type for which the DbType is to be determined. - /// DbType for the given system type. - public static DbType? GetDbTypeFromSystemType(Type systemType) - { - if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) - { - return null; - } - - return dbType; - } - - /// - /// Enables lookup of JsonDataType given a CLR Type. + /// Maps .NET Framework type (System/CLR type) to JsonDataType. /// private static Dictionary _systemTypeToJsonDataTypeMap = new() { @@ -82,6 +74,7 @@ public static class TypeHelper [typeof(char)] = JsonDataType.String, [typeof(Guid)] = JsonDataType.String, [typeof(byte[])] = JsonDataType.String, + [typeof(TimeSpan)] = JsonDataType.String, [typeof(byte?)] = JsonDataType.String, [typeof(sbyte?)] = JsonDataType.String, [typeof(short?)] = JsonDataType.Number, @@ -102,26 +95,111 @@ public static class TypeHelper }; /// - /// Converts the CLR type to JsonDataType - /// to meet the data type requirement set by the OpenAPI specification. - /// The value returned is formatted for the OpenAPI spec "type" property. + /// Maps SqlDbType enum to .NET Framework type (System type). + /// + private static readonly Dictionary _sqlDbTypeToType = new() + { + { SqlDbType.BigInt,typeof(long) }, + { SqlDbType.Binary, typeof(byte) }, + { SqlDbType.Bit, typeof(bool)}, + { SqlDbType.Char, typeof(string) }, + { SqlDbType.Date, typeof(DateTime) }, + { SqlDbType.DateTime, typeof(DateTime)}, + { SqlDbType.DateTime2, typeof(DateTime)}, + { SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, + { SqlDbType.Decimal, typeof(decimal)}, + { SqlDbType.Float, typeof(double)}, + { SqlDbType.Image, typeof(byte[])}, + { SqlDbType.Int, typeof(int)}, + { SqlDbType.Money, typeof(decimal)}, + { SqlDbType.NChar, typeof(char)}, + { SqlDbType.NText, typeof(string)}, + { SqlDbType.NVarChar,typeof(string) }, + { SqlDbType.Real, typeof(float)}, + { SqlDbType.SmallDateTime, typeof(DateTime) }, + { SqlDbType.SmallInt, typeof(short) }, + { SqlDbType.SmallMoney, typeof(decimal) }, + { SqlDbType.Text, typeof(string)}, + { SqlDbType.Time, typeof(TimeSpan)}, + { SqlDbType.Timestamp, typeof(byte[])}, + { SqlDbType.TinyInt, typeof(byte) }, + { SqlDbType.UniqueIdentifier, typeof(Guid) }, + { SqlDbType.VarBinary, typeof(byte[]) }, + { SqlDbType.VarChar, typeof(string) } + }; + + /// + /// Converts the .NET Framework (System/CLR) type to JsonDataType. + /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported + /// by the JSON Schema Specification Wright Draft 00. + /// The value returned is formatted for use in the OpenAPI spec "type" property. /// /// CLR type /// /// Formatted JSON type name in lower case: e.g. number, string, boolean, etc. - public static string SystemTypeToJsonDataType(Type type) + public static JsonDataType GetJsonDataTypeFromSystemType(Type type) { + // Get the underlying type argument if the 'type' argument is a nullable type. + Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullableUnderlyingType is not null) + { + type = nullableUnderlyingType; + } + if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName)) { openApiJsonTypeName = JsonDataType.Undefined; } - else + + return openApiJsonTypeName; + } + + /// + /// Returns the DbType for given system type. + /// + /// The system type for which the DbType is to be determined. + /// DbType for the given system type. + public static DbType? GetDbTypeFromSystemType(Type systemType) + { + if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) { - Console.Out.WriteLine("unknown type"); + return null; + } + + return dbType; + } + + /// + /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented + /// by the SQL Server data type mappings article. + /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. + /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. + /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however + /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. + /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', + /// however SqlDbType.Binary now exists. + /// + /// String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns + /// schema collections. + /// + /// + /// Failed type conversion." + public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName) + { + if (Enum.TryParse(enumType: typeof(SqlDbType), value: sqlDbTypeName, ignoreCase: true, out object? result) && result is not null) + { + SqlDbType sqlDbType = (SqlDbType)result; + + if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value)) + { + return value; + } } - string formattedOpenApiTypeName = openApiJsonTypeName.ToString().ToLower(); - return formattedOpenApiTypeName; + throw new DataApiBuilderException( + message: $"Tried to convert unsupported data type: {sqlDbTypeName}", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } } } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index fc29e15a0e..78232afab4 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -1423,7 +1423,7 @@ public async Task OpenApi_EntityLevelRestEndpoint() using TestServer server = new(Program.CreateWebHostBuilder(args)); using HttpClient client = server.CreateClient(); // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{OpenApiDocumentor.OPENAPI_ROUTE}"); + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); // Parse response metadata diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs new file mode 100644 index 0000000000..297f50ce9c --- /dev/null +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Data; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.DataApiBuilder.Service.Models; +using Azure.DataApiBuilder.Service.Services; +using Azure.DataApiBuilder.Service.Services.OpenAPI; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.OpenApiDocumentor +{ + /// + /// Validates TypeHelper converters return expected results. + /// + [TestClass] + public class CLRtoJsonValueTypeUnitTests + { + private const string ERROR_PREFIX = "The SqlDbType "; + private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; + private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; + private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; + + /// + /// Validates that all DAB supported CLR types (system types) map to a defined JSON value type. + /// A DAB supported CLR type is a CLR type mapped from a database value type. + /// + [TestMethod] + public void SupportedSystemTypesMapToJsonValueType() + { + foreach (KeyValuePair sqlDataType in SqlTypeConstants.SupportedSqlDbTypes) + { + string sqlDataTypeLiteral = sqlDataType.Key; + bool isSupportedSqlDataType = sqlDataType.Value; + + try + { + Type resolvedType = TypeHelper.GetSystemTypeFromSqlDbType(sqlDataTypeLiteral); + Assert.AreEqual(true, isSupportedSqlDataType, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + + JsonDataType resolvedJsonType = TypeHelper.GetJsonDataTypeFromSystemType(resolvedType); + Assert.AreEqual(isSupportedSqlDataType, resolvedJsonType != JsonDataType.Undefined, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + JSONDATATYPE_RESOLUTION_ERROR); + + DbType? resolvedDbType = TypeHelper.GetDbTypeFromSystemType(resolvedType); + Assert.AreEqual(isSupportedSqlDataType, resolvedDbType is not null, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + DBTYPE_RESOLUTION_ERROR); + } + catch (DataApiBuilderException) + { + Assert.AreEqual(false, isSupportedSqlDataType, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + } + } + } + } +} From 4296015af01c40e28d9c994ff0ddb6ebb832800d Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Wed, 5 Jul 2023 18:11:04 -0700 Subject: [PATCH 03/11] Cherry pick from working branch to mitigate big merge conflict resolution in another pr. --- src/Core/Models/SqlTypeConstants.cs | 2 -- src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs | 2 -- src/Core/Services/TypeHelper.cs | 2 ++ src/Service.Tests/Configuration/ConfigurationTests.cs | 3 +-- .../OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs | 4 ++-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs index 276578a147..8c9ac25aaa 100644 --- a/src/Core/Models/SqlTypeConstants.cs +++ b/src/Core/Models/SqlTypeConstants.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; - namespace Azure.DataApiBuilder.Service.Models { /// diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index e5a0ff1899..be6cae38e8 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Net; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Resolvers; -using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 3173346a2a..3f8913edad 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System.Data; +using System.Net; using Azure.DataApiBuilder.Core.Services.OpenAPI; +using Azure.DataApiBuilder.Service.Exceptions; namespace Azure.DataApiBuilder.Core.Services { diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 78232afab4..b5556e864c 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -24,7 +24,6 @@ using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; -using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Controllers; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Tests.Authorization; @@ -1423,7 +1422,7 @@ public async Task OpenApi_EntityLevelRestEndpoint() using TestServer server = new(Program.CreateWebHostBuilder(args)); using HttpClient client = server.CreateClient(); // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Core.Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); // Parse response metadata diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 297f50ce9c..cf0748c74a 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Data; +using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; -using Azure.DataApiBuilder.Service.Services; -using Azure.DataApiBuilder.Service.Services.OpenAPI; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.OpenApiDocumentor From 110afbe3fae6beb4e54a38439b2ff5f48c509829 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Thu, 6 Jul 2023 11:34:06 -0700 Subject: [PATCH 04/11] Addressing review feedback: used file scoped namespaces. updated enum.tryparse override used since latest syntax eliminates requirement to do null check and casting. Removed "DateTime" and "Time" from TypeHelper._systemTypeToDbTypeMap because they caused GraphQL type tests to fail because of exceptions converting from DateTimeOffset to DateTime. That issue may be fixed by #1473 --- src/Core/Models/SqlTypeConstants.cs | 89 +- src/Core/Services/TypeHelper.cs | 354 +- .../Configuration/ConfigurationTests.cs | 3025 ++++++++--------- .../CLRtoJsonValueTypeUnitTests.cs | 84 +- 4 files changed, 1778 insertions(+), 1774 deletions(-) diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs index 8c9ac25aaa..a435386715 100644 --- a/src/Core/Models/SqlTypeConstants.cs +++ b/src/Core/Models/SqlTypeConstants.cs @@ -1,53 +1,52 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Azure.DataApiBuilder.Service.Models +namespace Azure.DataApiBuilder.Service.Models; + +/// +/// String literal representation of SQL Server data types that are returned by Microsoft.Data.SqlClient. +/// +/// +public static class SqlTypeConstants { /// - /// String literal representation of SQL Server data types that are returned by Microsoft.Data.SqlClient. + /// SqlDbType names ordered by corresponding SqlDbType. + /// Keys are lower case to match formatting of SQL Server INFORMATION_SCHEMA DATA_TYPE column value. + /// Sourced directly from internal/private SmiMetaData.cs in Microsoft.Data.SqlClient + /// Value indicates whether DAB engine supports the SqlDbType. /// - /// - public class SqlTypeConstants + /// + public static readonly Dictionary SupportedSqlDbTypes = new() { - /// - /// SqlDbType names ordered by corresponding SqlDbType. - /// Keys are lower case to match formatting of SQL Server INFORMATION_SCHEMA DATA_TYPE column value. - /// Sourced directly from internal/private SmiMetaData.cs in Microsoft.Data.SqlClient - /// Value indicates whether DAB engine supports the SqlDbType. - /// - /// - public static readonly Dictionary SupportedSqlDbTypes = new() - { - { "bigint", true }, // SqlDbType.BigInt - { "binary", true }, // SqlDbType.Binary - { "bit", true }, // SqlDbType.Bit - { "char", true }, // SqlDbType.Char - { "datetime", true }, // SqlDbType.DateTime - { "decimal", true }, // SqlDbType.Decimal - { "float", true }, // SqlDbType.Float - { "image", true }, // SqlDbType.Image - { "int", true }, // SqlDbType.Int - { "money", true }, // SqlDbType.Money - { "nchar", true }, // SqlDbType.NChar - { "ntext", true }, // SqlDbType.NText - { "nvarchar", true }, // SqlDbType.NVarChar - { "real", true }, // SqlDbType.Real - { "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier - { "smalldatetime", true }, // SqlDbType.SmallDateTime - { "smallint", true }, // SqlDbType.SmallInt - { "smallmoney", true }, // SqlDbType.SmallMoney - { "text", true }, // SqlDbType.Text - { "timestamp", true }, // SqlDbType.Timestamp - { "tinyint", true }, // SqlDbType.TinyInt - { "varbinary", true }, // SqlDbType.VarBinary - { "varchar", true }, // SqlDbType.VarChar - { "sql_variant", false }, // SqlDbType.Variant (unsupported) - { "xml", false }, // SqlDbType.Xml (unsupported) - { "date", true }, // SqlDbType.Date - { "time", true }, // SqlDbType.Time - { "datetime2", true }, // SqlDbType.DateTime2 - { "datetimeoffset", true }, // SqlDbType.DateTimeOffset - { "", false }, // SqlDbType.Udt and SqlDbType.Structured (unsupported) - }; - } + { "bigint", true }, // SqlDbType.BigInt + { "binary", true }, // SqlDbType.Binary + { "bit", true }, // SqlDbType.Bit + { "char", true }, // SqlDbType.Char + { "datetime", true }, // SqlDbType.DateTime + { "decimal", true }, // SqlDbType.Decimal + { "float", true }, // SqlDbType.Float + { "image", true }, // SqlDbType.Image + { "int", true }, // SqlDbType.Int + { "money", true }, // SqlDbType.Money + { "nchar", true }, // SqlDbType.NChar + { "ntext", true }, // SqlDbType.NText + { "nvarchar", true }, // SqlDbType.NVarChar + { "real", true }, // SqlDbType.Real + { "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier + { "smalldatetime", true }, // SqlDbType.SmallDateTime + { "smallint", true }, // SqlDbType.SmallInt + { "smallmoney", true }, // SqlDbType.SmallMoney + { "text", true }, // SqlDbType.Text + { "timestamp", true }, // SqlDbType.Timestamp + { "tinyint", true }, // SqlDbType.TinyInt + { "varbinary", true }, // SqlDbType.VarBinary + { "varchar", true }, // SqlDbType.VarChar + { "sql_variant", false }, // SqlDbType.Variant (unsupported) + { "xml", false }, // SqlDbType.Xml (unsupported) + { "date", true }, // SqlDbType.Date + { "time", true }, // SqlDbType.Time + { "datetime2", true }, // SqlDbType.DateTime2 + { "datetimeoffset", true }, // SqlDbType.DateTimeOffset + { "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported) + }; } diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 3f8913edad..4030d0b919 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -6,202 +6,198 @@ using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; -namespace Azure.DataApiBuilder.Core.Services +namespace Azure.DataApiBuilder.Core.Services; + +/// +/// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types. +/// +/// +public static class TypeHelper { /// - /// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types. + /// Maps .NET Framework types to DbType enum + /// Unnecessary to add nullable types because /// - /// - public static class TypeHelper + private static Dictionary _systemTypeToDbTypeMap = new() { - /// - /// Maps .NET Framework types to DbType enum - /// - private static Dictionary _systemTypeToDbTypeMap = new() - { - [typeof(byte)] = DbType.Byte, - [typeof(sbyte)] = DbType.SByte, - [typeof(short)] = DbType.Int16, - [typeof(ushort)] = DbType.UInt16, - [typeof(int)] = DbType.Int32, - [typeof(uint)] = DbType.UInt32, - [typeof(long)] = DbType.Int64, - [typeof(ulong)] = DbType.UInt64, - [typeof(float)] = DbType.Single, - [typeof(double)] = DbType.Double, - [typeof(decimal)] = DbType.Decimal, - [typeof(bool)] = DbType.Boolean, - [typeof(string)] = DbType.String, - [typeof(char)] = DbType.StringFixedLength, - [typeof(Guid)] = DbType.Guid, - [typeof(byte[])] = DbType.Binary, - [typeof(DateTime)] = DbType.DateTime, - [typeof(DateTimeOffset)] = DbType.DateTimeOffset, - [typeof(TimeSpan)] = DbType.Time, - [typeof(byte?)] = DbType.Byte, - [typeof(sbyte?)] = DbType.SByte, - [typeof(short?)] = DbType.Int16, - [typeof(ushort?)] = DbType.UInt16, - [typeof(int?)] = DbType.Int32, - [typeof(uint?)] = DbType.UInt32, - [typeof(long?)] = DbType.Int64, - [typeof(ulong?)] = DbType.UInt64, - [typeof(float?)] = DbType.Single, - [typeof(double?)] = DbType.Double, - [typeof(decimal?)] = DbType.Decimal, - [typeof(bool?)] = DbType.Boolean, - [typeof(char?)] = DbType.StringFixedLength, - [typeof(Guid?)] = DbType.Guid, - [typeof(object)] = DbType.Object - }; + [typeof(byte)] = DbType.Byte, + [typeof(sbyte)] = DbType.SByte, + [typeof(short)] = DbType.Int16, + [typeof(ushort)] = DbType.UInt16, + [typeof(int)] = DbType.Int32, + [typeof(uint)] = DbType.UInt32, + [typeof(long)] = DbType.Int64, + [typeof(ulong)] = DbType.UInt64, + [typeof(float)] = DbType.Single, + [typeof(double)] = DbType.Double, + [typeof(decimal)] = DbType.Decimal, + [typeof(bool)] = DbType.Boolean, + [typeof(string)] = DbType.String, + [typeof(char)] = DbType.StringFixedLength, + [typeof(Guid)] = DbType.Guid, + [typeof(byte[])] = DbType.Binary, + [typeof(DateTimeOffset)] = DbType.DateTimeOffset, + [typeof(byte?)] = DbType.Byte, + [typeof(sbyte?)] = DbType.SByte, + [typeof(short?)] = DbType.Int16, + [typeof(ushort?)] = DbType.UInt16, + [typeof(int?)] = DbType.Int32, + [typeof(uint?)] = DbType.UInt32, + [typeof(long?)] = DbType.Int64, + [typeof(ulong?)] = DbType.UInt64, + [typeof(float?)] = DbType.Single, + [typeof(double?)] = DbType.Double, + [typeof(decimal?)] = DbType.Decimal, + [typeof(bool?)] = DbType.Boolean, + [typeof(char?)] = DbType.StringFixedLength, + [typeof(Guid?)] = DbType.Guid, + [typeof(object)] = DbType.Object + }; - /// - /// Maps .NET Framework type (System/CLR type) to JsonDataType. - /// - private static Dictionary _systemTypeToJsonDataTypeMap = new() - { - [typeof(byte)] = JsonDataType.String, - [typeof(sbyte)] = JsonDataType.String, - [typeof(short)] = JsonDataType.Number, - [typeof(ushort)] = JsonDataType.Number, - [typeof(int)] = JsonDataType.Number, - [typeof(uint)] = JsonDataType.Number, - [typeof(long)] = JsonDataType.Number, - [typeof(ulong)] = JsonDataType.Number, - [typeof(float)] = JsonDataType.Number, - [typeof(double)] = JsonDataType.Number, - [typeof(decimal)] = JsonDataType.Number, - [typeof(bool)] = JsonDataType.Boolean, - [typeof(string)] = JsonDataType.String, - [typeof(char)] = JsonDataType.String, - [typeof(Guid)] = JsonDataType.String, - [typeof(byte[])] = JsonDataType.String, - [typeof(TimeSpan)] = JsonDataType.String, - [typeof(byte?)] = JsonDataType.String, - [typeof(sbyte?)] = JsonDataType.String, - [typeof(short?)] = JsonDataType.Number, - [typeof(ushort?)] = JsonDataType.Number, - [typeof(int?)] = JsonDataType.Number, - [typeof(uint?)] = JsonDataType.Number, - [typeof(long?)] = JsonDataType.Number, - [typeof(ulong?)] = JsonDataType.Number, - [typeof(float?)] = JsonDataType.Number, - [typeof(double?)] = JsonDataType.Number, - [typeof(decimal?)] = JsonDataType.Number, - [typeof(bool?)] = JsonDataType.Boolean, - [typeof(char?)] = JsonDataType.String, - [typeof(Guid?)] = JsonDataType.String, - [typeof(object)] = JsonDataType.Object, - [typeof(DateTime)] = JsonDataType.String, - [typeof(DateTimeOffset)] = JsonDataType.String - }; + /// + /// Maps .NET Framework type (System/CLR type) to JsonDataType. + /// + private static Dictionary _systemTypeToJsonDataTypeMap = new() + { + [typeof(byte)] = JsonDataType.String, + [typeof(sbyte)] = JsonDataType.String, + [typeof(short)] = JsonDataType.Number, + [typeof(ushort)] = JsonDataType.Number, + [typeof(int)] = JsonDataType.Number, + [typeof(uint)] = JsonDataType.Number, + [typeof(long)] = JsonDataType.Number, + [typeof(ulong)] = JsonDataType.Number, + [typeof(float)] = JsonDataType.Number, + [typeof(double)] = JsonDataType.Number, + [typeof(decimal)] = JsonDataType.Number, + [typeof(bool)] = JsonDataType.Boolean, + [typeof(string)] = JsonDataType.String, + [typeof(char)] = JsonDataType.String, + [typeof(Guid)] = JsonDataType.String, + [typeof(byte[])] = JsonDataType.String, + [typeof(TimeSpan)] = JsonDataType.String, + [typeof(byte?)] = JsonDataType.String, + [typeof(sbyte?)] = JsonDataType.String, + [typeof(short?)] = JsonDataType.Number, + [typeof(ushort?)] = JsonDataType.Number, + [typeof(int?)] = JsonDataType.Number, + [typeof(uint?)] = JsonDataType.Number, + [typeof(long?)] = JsonDataType.Number, + [typeof(ulong?)] = JsonDataType.Number, + [typeof(float?)] = JsonDataType.Number, + [typeof(double?)] = JsonDataType.Number, + [typeof(decimal?)] = JsonDataType.Number, + [typeof(bool?)] = JsonDataType.Boolean, + [typeof(char?)] = JsonDataType.String, + [typeof(Guid?)] = JsonDataType.String, + [typeof(object)] = JsonDataType.Object, + [typeof(DateTime)] = JsonDataType.String, + [typeof(DateTimeOffset)] = JsonDataType.String + }; - /// - /// Maps SqlDbType enum to .NET Framework type (System type). - /// - private static readonly Dictionary _sqlDbTypeToType = new() - { - { SqlDbType.BigInt,typeof(long) }, - { SqlDbType.Binary, typeof(byte) }, - { SqlDbType.Bit, typeof(bool)}, - { SqlDbType.Char, typeof(string) }, - { SqlDbType.Date, typeof(DateTime) }, - { SqlDbType.DateTime, typeof(DateTime)}, - { SqlDbType.DateTime2, typeof(DateTime)}, - { SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, - { SqlDbType.Decimal, typeof(decimal)}, - { SqlDbType.Float, typeof(double)}, - { SqlDbType.Image, typeof(byte[])}, - { SqlDbType.Int, typeof(int)}, - { SqlDbType.Money, typeof(decimal)}, - { SqlDbType.NChar, typeof(char)}, - { SqlDbType.NText, typeof(string)}, - { SqlDbType.NVarChar,typeof(string) }, - { SqlDbType.Real, typeof(float)}, - { SqlDbType.SmallDateTime, typeof(DateTime) }, - { SqlDbType.SmallInt, typeof(short) }, - { SqlDbType.SmallMoney, typeof(decimal) }, - { SqlDbType.Text, typeof(string)}, - { SqlDbType.Time, typeof(TimeSpan)}, - { SqlDbType.Timestamp, typeof(byte[])}, - { SqlDbType.TinyInt, typeof(byte) }, - { SqlDbType.UniqueIdentifier, typeof(Guid) }, - { SqlDbType.VarBinary, typeof(byte[]) }, - { SqlDbType.VarChar, typeof(string) } - }; + /// + /// Maps SqlDbType enum to .NET Framework type (System type). + /// + private static readonly Dictionary _sqlDbTypeToType = new() + { + { SqlDbType.BigInt,typeof(long) }, + { SqlDbType.Binary, typeof(byte) }, + { SqlDbType.Bit, typeof(bool)}, + { SqlDbType.Char, typeof(string) }, + { SqlDbType.Date, typeof(DateTime) }, + { SqlDbType.DateTime, typeof(DateTime)}, + { SqlDbType.DateTime2, typeof(DateTime)}, + { SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, + { SqlDbType.Decimal, typeof(decimal)}, + { SqlDbType.Float, typeof(double)}, + { SqlDbType.Image, typeof(byte[])}, + { SqlDbType.Int, typeof(int)}, + { SqlDbType.Money, typeof(decimal)}, + { SqlDbType.NChar, typeof(char)}, + { SqlDbType.NText, typeof(string)}, + { SqlDbType.NVarChar,typeof(string) }, + { SqlDbType.Real, typeof(float)}, + { SqlDbType.SmallDateTime, typeof(DateTime) }, + { SqlDbType.SmallInt, typeof(short) }, + { SqlDbType.SmallMoney, typeof(decimal) }, + { SqlDbType.Text, typeof(string)}, + { SqlDbType.Time, typeof(TimeSpan)}, + { SqlDbType.Timestamp, typeof(byte[])}, + { SqlDbType.TinyInt, typeof(byte) }, + { SqlDbType.UniqueIdentifier, typeof(Guid) }, + { SqlDbType.VarBinary, typeof(byte[]) }, + { SqlDbType.VarChar, typeof(string) } + }; - /// - /// Converts the .NET Framework (System/CLR) type to JsonDataType. - /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported - /// by the JSON Schema Specification Wright Draft 00. - /// The value returned is formatted for use in the OpenAPI spec "type" property. - /// - /// CLR type - /// - /// Formatted JSON type name in lower case: e.g. number, string, boolean, etc. - public static JsonDataType GetJsonDataTypeFromSystemType(Type type) + /// + /// Converts the .NET Framework (System/CLR) type to JsonDataType. + /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported + /// by the JSON Schema Specification Wright Draft 00. + /// The value returned is formatted for use in the OpenAPI spec "type" property. + /// + /// CLR type + /// + /// Formatted JSON type name in lower case: e.g. number, string, boolean, etc. + public static JsonDataType GetJsonDataTypeFromSystemType(Type type) + { + // Get the underlying type argument if the 'type' argument is a nullable type. + Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullableUnderlyingType is not null) { - // Get the underlying type argument if the 'type' argument is a nullable type. - Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); - if (nullableUnderlyingType is not null) - { - type = nullableUnderlyingType; - } - - if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName)) - { - openApiJsonTypeName = JsonDataType.Undefined; - } - - return openApiJsonTypeName; + type = nullableUnderlyingType; } - /// - /// Returns the DbType for given system type. - /// - /// The system type for which the DbType is to be determined. - /// DbType for the given system type. - public static DbType? GetDbTypeFromSystemType(Type systemType) + if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName)) { - if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) - { - return null; - } + openApiJsonTypeName = JsonDataType.Undefined; + } - return dbType; + return openApiJsonTypeName; + } + + /// + /// Returns the DbType for given system type. + /// + /// The system type for which the DbType is to be determined. + /// DbType for the given system type. Null when no mapping exists. + public static DbType? GetDbTypeFromSystemType(Type systemType) + { + if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) + { + return null; } - /// - /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented - /// by the SQL Server data type mappings article. - /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. - /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. - /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however - /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. - /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', - /// however SqlDbType.Binary now exists. - /// - /// String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns - /// schema collections. - /// - /// - /// Failed type conversion." - public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName) + return dbType; + } + + /// + /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented + /// by the SQL Server data type mappings article. + /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. + /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. + /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however + /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. + /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', + /// however SqlDbType.Binary now exists. + /// + /// String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns + /// schema collections. + /// + /// + /// Failed type conversion." + public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName) + { + if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType)) { - if (Enum.TryParse(enumType: typeof(SqlDbType), value: sqlDbTypeName, ignoreCase: true, out object? result) && result is not null) + if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value)) { - SqlDbType sqlDbType = (SqlDbType)result; - - if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value)) - { - return value; - } + return value; } - - throw new DataApiBuilderException( - message: $"Tried to convert unsupported data type: {sqlDbTypeName}", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } + + throw new DataApiBuilderException( + message: $"Tried to convert unsupported data type: {sqlDbTypeName}", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); } } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index b5556e864c..14e73e19f1 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -41,797 +41,797 @@ using VerifyMSTest; using static Azure.DataApiBuilder.Config.RuntimeConfigLoader; -namespace Azure.DataApiBuilder.Service.Tests.Configuration +namespace Azure.DataApiBuilder.Service.Tests.Configuration; + +[TestClass] +public class ConfigurationTests + : VerifyBase { - [TestClass] - public class ConfigurationTests - : VerifyBase - { - private const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL; - private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; - private const string MYSQL_ENVIRONMENT = TestCategory.MYSQL; - private const string POSTGRESQL_ENVIRONMENT = TestCategory.POSTGRESQL; - private const string POST_STARTUP_CONFIG_ENTITY = "Book"; - private const string POST_STARTUP_CONFIG_ENTITY_SOURCE = "books"; - private const string POST_STARTUP_CONFIG_ROLE = "PostStartupConfigRole"; - private const string COSMOS_DATABASE_NAME = "config_db"; - private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; - private const string OPENAPI_SWAGGER_ENDPOINT = "swagger"; - private const string OPENAPI_DOCUMENT_ENDPOINT = "openapi"; - private const string BROWSER_USER_AGENT_HEADER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"; - private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; - - private const int RETRY_COUNT = 5; - private const int RETRY_WAIT_SECONDS = 1; - - // TODO: Remove the old endpoint once we've updated all callers to use the new one. - private const string CONFIGURATION_ENDPOINT = "/configuration"; - private const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2"; - - /// - /// A valid REST API request body with correct parameter types for all the fields. - /// - public const string REQUEST_BODY_WITH_CORRECT_PARAM_TYPES = @" + private const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL; + private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; + private const string MYSQL_ENVIRONMENT = TestCategory.MYSQL; + private const string POSTGRESQL_ENVIRONMENT = TestCategory.POSTGRESQL; + private const string POST_STARTUP_CONFIG_ENTITY = "Book"; + private const string POST_STARTUP_CONFIG_ENTITY_SOURCE = "books"; + private const string POST_STARTUP_CONFIG_ROLE = "PostStartupConfigRole"; + private const string COSMOS_DATABASE_NAME = "config_db"; + private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; + private const string OPENAPI_SWAGGER_ENDPOINT = "swagger"; + private const string OPENAPI_DOCUMENT_ENDPOINT = "openapi"; + private const string BROWSER_USER_AGENT_HEADER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"; + private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; + + private const int RETRY_COUNT = 5; + private const int RETRY_WAIT_SECONDS = 1; + + // TODO: Remove the old endpoint once we've updated all callers to use the new one. + private const string CONFIGURATION_ENDPOINT = "/configuration"; + private const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2"; + + /// + /// A valid REST API request body with correct parameter types for all the fields. + /// + public const string REQUEST_BODY_WITH_CORRECT_PARAM_TYPES = @" { ""title"": ""New book"", ""publisher_id"": 1234 } "; - /// - /// An invalid REST API request body with incorrect parameter type for publisher_id field. - /// - public const string REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES = @" + /// + /// An invalid REST API request body with incorrect parameter type for publisher_id field. + /// + public const string REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES = @" { ""title"": ""New book"", ""publisher_id"": ""one"" } "; - [TestCleanup] - public void CleanupAfterEachTest() - { - TestHelper.UnsetAllDABEnvironmentVariables(); - } + [TestCleanup] + public void CleanupAfterEachTest() + { + TestHelper.UnsetAllDABEnvironmentVariables(); + } - /// - /// When updating config during runtime is possible, then For invalid config the Application continues to - /// accept request with status code of 503. - /// But if invalid config is provided during startup, ApplicationException is thrown - /// and application exits. - /// - [DataTestMethod] - [DataRow(new string[] { }, true, DisplayName = "No config returns 503 - config file flag absent")] - [DataRow(new string[] { "--ConfigFileName=" }, true, DisplayName = "No config returns 503 - empty config file option")] - [DataRow(new string[] { }, false, DisplayName = "Throws Application exception")] - [TestMethod("Validates that queries before runtime is configured returns a 503 in hosting scenario whereas an application exception when run through CLI")] - public async Task TestNoConfigReturnsServiceUnavailable( - string[] args, - bool isUpdateableRuntimeConfig) - { - TestServer server; + /// + /// When updating config during runtime is possible, then For invalid config the Application continues to + /// accept request with status code of 503. + /// But if invalid config is provided during startup, ApplicationException is thrown + /// and application exits. + /// + [DataTestMethod] + [DataRow(new string[] { }, true, DisplayName = "No config returns 503 - config file flag absent")] + [DataRow(new string[] { "--ConfigFileName=" }, true, DisplayName = "No config returns 503 - empty config file option")] + [DataRow(new string[] { }, false, DisplayName = "Throws Application exception")] + [TestMethod("Validates that queries before runtime is configured returns a 503 in hosting scenario whereas an application exception when run through CLI")] + public async Task TestNoConfigReturnsServiceUnavailable( + string[] args, + bool isUpdateableRuntimeConfig) + { + TestServer server; - try + try + { + if (isUpdateableRuntimeConfig) { - if (isUpdateableRuntimeConfig) - { - server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(args)); - } - else - { - server = new(Program.CreateWebHostBuilder(args)); - } - - HttpClient httpClient = server.CreateClient(); - HttpResponseMessage result = await httpClient.GetAsync("/graphql"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, result.StatusCode); + server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(args)); } - catch (Exception e) + else { - Assert.IsFalse(isUpdateableRuntimeConfig); - Assert.AreEqual(typeof(ApplicationException), e.GetType()); - Assert.AreEqual( - $"Could not initialize the engine with the runtime config file: {DEFAULT_CONFIG_FILE_NAME}", - e.Message); + server = new(Program.CreateWebHostBuilder(args)); } - } - /// - /// Verify that https redirection is disabled when --no-https-redirect flag is passed through CLI. - /// We check if IsHttpsRedirectionDisabled is set to true with --no-https-redirect flag. - /// - [DataTestMethod] - [DataRow(new string[] { "" }, false, DisplayName = "Https redirection allowed")] - [DataRow(new string[] { Startup.NO_HTTPS_REDIRECT_FLAG }, true, DisplayName = "Http redirection disabled")] - [TestMethod("Validates that https redirection is disabled when --no-https-redirect option is used when engine is started through CLI")] - public void TestDisablingHttpsRedirection( - string[] args, - bool expectedIsHttpsRedirectionDisabled) + HttpClient httpClient = server.CreateClient(); + HttpResponseMessage result = await httpClient.GetAsync("/graphql"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, result.StatusCode); + } + catch (Exception e) { - Program.CreateWebHostBuilder(args).Build(); - Assert.AreEqual(expectedIsHttpsRedirectionDisabled, Program.IsHttpsRedirectionDisabled); + Assert.IsFalse(isUpdateableRuntimeConfig); + Assert.AreEqual(typeof(ApplicationException), e.GetType()); + Assert.AreEqual( + $"Could not initialize the engine with the runtime config file: {DEFAULT_CONFIG_FILE_NAME}", + e.Message); } + } + + /// + /// Verify that https redirection is disabled when --no-https-redirect flag is passed through CLI. + /// We check if IsHttpsRedirectionDisabled is set to true with --no-https-redirect flag. + /// + [DataTestMethod] + [DataRow(new string[] { "" }, false, DisplayName = "Https redirection allowed")] + [DataRow(new string[] { Startup.NO_HTTPS_REDIRECT_FLAG }, true, DisplayName = "Http redirection disabled")] + [TestMethod("Validates that https redirection is disabled when --no-https-redirect option is used when engine is started through CLI")] + public void TestDisablingHttpsRedirection( + string[] args, + bool expectedIsHttpsRedirectionDisabled) + { + Program.CreateWebHostBuilder(args).Build(); + Assert.AreEqual(expectedIsHttpsRedirectionDisabled, Program.IsHttpsRedirectionDisabled); + } - /// - /// Checks correct serialization and deserialization of Source Type from - /// Enum to String and vice-versa. - /// Consider both cases for source as an object and as a string - /// - [DataTestMethod] - [DataRow(true, EntitySourceType.StoredProcedure, "stored-procedure", DisplayName = "source is a stored-procedure")] - [DataRow(true, EntitySourceType.Table, "table", DisplayName = "source is a table")] - [DataRow(true, EntitySourceType.View, "view", DisplayName = "source is a view")] - [DataRow(false, null, null, DisplayName = "source is just string")] - public void TestCorrectSerializationOfSourceObject( - bool isDatabaseObjectSource, - EntitySourceType sourceObjectType, - string sourceTypeName) + /// + /// Checks correct serialization and deserialization of Source Type from + /// Enum to String and vice-versa. + /// Consider both cases for source as an object and as a string + /// + [DataTestMethod] + [DataRow(true, EntitySourceType.StoredProcedure, "stored-procedure", DisplayName = "source is a stored-procedure")] + [DataRow(true, EntitySourceType.Table, "table", DisplayName = "source is a table")] + [DataRow(true, EntitySourceType.View, "view", DisplayName = "source is a view")] + [DataRow(false, null, null, DisplayName = "source is just string")] + public void TestCorrectSerializationOfSourceObject( + bool isDatabaseObjectSource, + EntitySourceType sourceObjectType, + string sourceTypeName) + { + RuntimeConfig runtimeConfig; + if (isDatabaseObjectSource) { - RuntimeConfig runtimeConfig; - if (isDatabaseObjectSource) - { - EntitySource entitySource = new( - Type: sourceObjectType, - Object: "sourceName", - Parameters: null, - KeyFields: null - ); - runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( - entityName: "MyEntity", - entitySource: entitySource, - roleName: "Anonymous", - operation: EntityActionOperation.All - ); - } - else - { - string entitySource = "sourceName"; - runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( - entityName: "MyEntity", - entitySource: entitySource, - roleName: "Anonymous", - operation: EntityActionOperation.All - ); - } + EntitySource entitySource = new( + Type: sourceObjectType, + Object: "sourceName", + Parameters: null, + KeyFields: null + ); + runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( + entityName: "MyEntity", + entitySource: entitySource, + roleName: "Anonymous", + operation: EntityActionOperation.All + ); + } + else + { + string entitySource = "sourceName"; + runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( + entityName: "MyEntity", + entitySource: entitySource, + roleName: "Anonymous", + operation: EntityActionOperation.All + ); + } - string runtimeConfigJson = runtimeConfig.ToJson(); + string runtimeConfigJson = runtimeConfig.ToJson(); - if (isDatabaseObjectSource) - { - Assert.IsTrue(runtimeConfigJson.Contains(sourceTypeName)); - } + if (isDatabaseObjectSource) + { + Assert.IsTrue(runtimeConfigJson.Contains(sourceTypeName)); + } - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(runtimeConfigJson, out RuntimeConfig deserializedRuntimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(runtimeConfigJson, out RuntimeConfig deserializedRuntimeConfig)); - Assert.IsTrue(deserializedRuntimeConfig.Entities.ContainsKey("MyEntity")); - Assert.AreEqual("sourceName", deserializedRuntimeConfig.Entities["MyEntity"].Source.Object); + Assert.IsTrue(deserializedRuntimeConfig.Entities.ContainsKey("MyEntity")); + Assert.AreEqual("sourceName", deserializedRuntimeConfig.Entities["MyEntity"].Source.Object); - if (isDatabaseObjectSource) - { - Assert.AreEqual(sourceObjectType, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); - } - else - { - Assert.AreEqual(EntitySourceType.Table, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); - } + if (isDatabaseObjectSource) + { + Assert.AreEqual(sourceObjectType, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); } - - [TestMethod("Validates that once the configuration is set, the config controller isn't reachable."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestConflictAlreadySetConfiguration(string configurationEndpoint) + else { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); - - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - - _ = await httpClient.PostAsync(configurationEndpoint, content); - ValidateCosmosDbSetup(server); - - HttpResponseMessage result = await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); + Assert.AreEqual(EntitySourceType.Table, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); } + } - [TestMethod("Validates that the config controller returns a conflict when using local configuration."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestConflictLocalConfiguration(string configurationEndpoint) - { - Environment.SetEnvironmentVariable - (ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + [TestMethod("Validates that once the configuration is set, the config controller isn't reachable."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestConflictAlreadySetConfiguration(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - ValidateCosmosDbSetup(server); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + _ = await httpClient.PostAsync(configurationEndpoint, content); + ValidateCosmosDbSetup(server); - HttpResponseMessage result = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); - } + HttpResponseMessage result = await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); + } - [TestMethod("Validates setting the configuration at runtime."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + [TestMethod("Validates that the config controller returns a conflict when using local configuration."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestConflictLocalConfiguration(string configurationEndpoint) + { + Environment.SetEnvironmentVariable + (ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + ValidateCosmosDbSetup(server); - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - } + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - [TestMethod("Validates an invalid configuration returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestInvalidConfigurationAtRuntime(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + HttpResponseMessage result = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); + } - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, "invalidString"); + [TestMethod("Validates setting the configuration at runtime."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); - } + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - [TestMethod("Validates a failure in one of the config updated handlers returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingFailureConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + } - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + [TestMethod("Validates an invalid configuration returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestInvalidConfigurationAtRuntime(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); - runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((_, _) => - { - return Task.FromResult(false); - }); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, "invalidString"); - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); + } - Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); - } + [TestMethod("Validates a failure in one of the config updated handlers returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingFailureConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates that the configuration endpoint doesn't return until all configuration loaded handlers have executed."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestLongRunningConfigUpdatedHandlerConfigurations(string configurationEndpoint) + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + + RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); + runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((_, _) => { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + return Task.FromResult(false); + }); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); - RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); - bool taskHasCompleted = false; - runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) => - { - await Task.Delay(1000); - taskHasCompleted = true; - return true; - }); + Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); + } - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); + [TestMethod("Validates that the configuration endpoint doesn't return until all configuration loaded handlers have executed."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestLongRunningConfigUpdatedHandlerConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - Assert.IsTrue(taskHasCompleted); - } + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - /// - /// Tests that sending configuration to the DAB engine post-startup will properly hydrate - /// the AuthorizationResolver by: - /// 1. Validate that pre-configuration hydration requests result in 503 Service Unavailable - /// 2. Validate that custom configuration hydration succeeds. - /// 3. Validate that request to protected entity without role membership triggers Authorization Resolver - /// to reject the request with HTTP 403 Forbidden. - /// 4. Validate that request to protected entity with required role membership passes authorization requirements - /// and succeeds with HTTP 200 OK. - /// Note: This test is database engine agnostic, though requires denoting a database environment to fetch a usable - /// connection string to complete the test. Most applicable to CI/CD test execution. - /// - [TestCategory(TestCategory.MSSQL)] - [TestMethod("Validates setting the AuthN/Z configuration post-startup during runtime.")] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSqlSettingPostStartupConfigurations(string configurationEndpoint) + RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); + bool taskHasCompleted = false; + runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) => { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + await Task.Delay(1000); + taskHasCompleted = true; + return true; + }); - RuntimeConfig configuration = AuthorizationHelpers.InitRuntimeConfig( - entityName: POST_STARTUP_CONFIG_ENTITY, - entitySource: POST_STARTUP_CONFIG_ENTITY_SOURCE, - roleName: POST_STARTUP_CONFIG_ROLE, - operation: EntityActionOperation.Read, - includedCols: new HashSet() { "*" }); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + Assert.IsTrue(taskHasCompleted); + } - HttpResponseMessage preConfigHydrationResult = - await httpClient.GetAsync($"/{POST_STARTUP_CONFIG_ENTITY}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigHydrationResult.StatusCode); + /// + /// Tests that sending configuration to the DAB engine post-startup will properly hydrate + /// the AuthorizationResolver by: + /// 1. Validate that pre-configuration hydration requests result in 503 Service Unavailable + /// 2. Validate that custom configuration hydration succeeds. + /// 3. Validate that request to protected entity without role membership triggers Authorization Resolver + /// to reject the request with HTTP 403 Forbidden. + /// 4. Validate that request to protected entity with required role membership passes authorization requirements + /// and succeeds with HTTP 200 OK. + /// Note: This test is database engine agnostic, though requires denoting a database environment to fetch a usable + /// connection string to complete the test. Most applicable to CI/CD test execution. + /// + [TestCategory(TestCategory.MSSQL)] + [TestMethod("Validates setting the AuthN/Z configuration post-startup during runtime.")] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSqlSettingPostStartupConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); + + RuntimeConfig configuration = AuthorizationHelpers.InitRuntimeConfig( + entityName: POST_STARTUP_CONFIG_ENTITY, + entitySource: POST_STARTUP_CONFIG_ENTITY_SOURCE, + roleName: POST_STARTUP_CONFIG_ROLE, + operation: EntityActionOperation.Read, + includedCols: new HashSet() { "*" }); + + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + + HttpResponseMessage preConfigHydrationResult = + await httpClient.GetAsync($"/{POST_STARTUP_CONFIG_ENTITY}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigHydrationResult.StatusCode); + + HttpResponseMessage preConfigOpenApiDocumentExistence = + await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiDocumentExistence.StatusCode); + + // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. + HttpResponseMessage preConfigOpenApiSwaggerEndpointAvailability = + await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); + + HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); + + // When the authorization resolver is properly configured, authorization will have failed + // because no auth headers are present. + Assert.AreEqual( + expected: HttpStatusCode.Forbidden, + actual: responseCode, + message: "Configuration not yet hydrated after retry attempts.."); + + // Sends a GET request to a protected entity which requires a specific role to access. + // Authorization will pass because proper auth headers are present. + HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"api/{POST_STARTUP_CONFIG_ENTITY}"); + string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( + addAuthenticated: true, + specificRole: POST_STARTUP_CONFIG_ROLE); + message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); + message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); + HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); + + // OpenAPI document is created during config hydration and + // is made available after config hydration completes. + HttpResponseMessage postConfigOpenApiDocumentExistence = + await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.OK, postConfigOpenApiDocumentExistence.StatusCode); + + // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. + // HTTP 400 - BadRequest because when SwaggerUI is disabled, the endpoint is not mapped + // and the request is processed and failed by the RestService. + HttpResponseMessage postConfigOpenApiSwaggerEndpointAvailability = + await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.BadRequest, postConfigOpenApiSwaggerEndpointAvailability.StatusCode); + } - HttpResponseMessage preConfigOpenApiDocumentExistence = - await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiDocumentExistence.StatusCode); + [TestMethod("Validates that local CosmosDB_NoSQL settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestLoadingLocalCosmosSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. - HttpResponseMessage preConfigOpenApiSwaggerEndpointAvailability = - await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); + ValidateCosmosDbSetup(server); + } - HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); + [TestMethod("Validates access token is correctly loaded when Account Key is not present for Cosmos."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestLoadingAccessTokenForCosmosClient(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - // When the authorization resolver is properly configured, authorization will have failed - // because no auth headers are present. - Assert.AreEqual( - expected: HttpStatusCode.Forbidden, - actual: responseCode, - message: "Configuration not yet hydrated after retry attempts.."); - - // Sends a GET request to a protected entity which requires a specific role to access. - // Authorization will pass because proper auth headers are present. - HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"api/{POST_STARTUP_CONFIG_ENTITY}"); - string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - specificRole: POST_STARTUP_CONFIG_ROLE); - message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); - message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); - HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); - Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); - - // OpenAPI document is created during config hydration and - // is made available after config hydration completes. - HttpResponseMessage postConfigOpenApiDocumentExistence = - await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.OK, postConfigOpenApiDocumentExistence.StatusCode); - - // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. - // HTTP 400 - BadRequest because when SwaggerUI is disabled, the endpoint is not mapped - // and the request is processed and failed by the RestService. - HttpResponseMessage postConfigOpenApiSwaggerEndpointAvailability = - await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.BadRequest, postConfigOpenApiSwaggerEndpointAvailability.StatusCode); - } + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, null, true); - [TestMethod("Validates that local CosmosDB_NoSQL settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestLoadingLocalCosmosSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpResponseMessage authorizedResponse = await httpClient.PostAsync(configurationEndpoint, content); - ValidateCosmosDbSetup(server); - } + Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); + CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.IsNotNull(cosmosClientProvider); + Assert.IsNotNull(cosmosClientProvider.Client); + } - [TestMethod("Validates access token is correctly loaded when Account Key is not present for Cosmos."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestLoadingAccessTokenForCosmosClient(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + [TestMethod("Validates that local MsSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MSSQL)] + public void TestLoadingLocalMsSqlSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, null, true); + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - HttpResponseMessage authorizedResponse = await httpClient.PostAsync(configurationEndpoint, content); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); - CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.IsNotNull(cosmosClientProvider); - Assert.IsNotNull(cosmosClientProvider.Client); - } + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(MsSqlQueryBuilder)); - [TestMethod("Validates that local MsSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MSSQL)] - public void TestLoadingLocalMsSqlSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MsSqlMetadataProvider)); + } - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + [TestMethod("Validates that local PostgreSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.POSTGRESQL)] + public void TestLoadingLocalPostgresSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, POSTGRESQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(MsSqlQueryBuilder)); + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MsSqlMetadataProvider)); - } + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(PostgresQueryBuilder)); - [TestMethod("Validates that local PostgreSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.POSTGRESQL)] - public void TestLoadingLocalPostgresSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, POSTGRESQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(PostgreSqlMetadataProvider)); + } - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + [TestMethod("Validates that local MySql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MYSQL)] + public void TestLoadingLocalMySqlSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MYSQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(PostgresQueryBuilder)); + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(PostgreSqlMetadataProvider)); - } + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(MySqlQueryBuilder)); - [TestMethod("Validates that local MySql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MYSQL)] - public void TestLoadingLocalMySqlSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MYSQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MySqlMetadataProvider)); + } - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + [TestMethod("Validates that trying to override configs that are already set fail."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestOverridingLocalSettingsFails(string configurationEndpoint) + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpClient client = server.CreateClient(); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(MySqlQueryBuilder)); + JsonContent config = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, config); + Assert.AreEqual(HttpStatusCode.Conflict, postResult.StatusCode); + } - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MySqlMetadataProvider)); - } + [TestMethod("Validates that setting the configuration at runtime will instantiate the proper classes."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingConfigurationCreatesCorrectClasses(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient client = server.CreateClient(); - [TestMethod("Validates that trying to override configs that are already set fail."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestOverridingLocalSettingsFails(string configurationEndpoint) - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - HttpClient client = server.CreateClient(); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - JsonContent config = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, config); - Assert.AreEqual(HttpStatusCode.Conflict, postResult.StatusCode); - } + ValidateCosmosDbSetup(server); + RuntimeConfigProvider configProvider = server.Services.GetService(); - [TestMethod("Validates that setting the configuration at runtime will instantiate the proper classes."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingConfigurationCreatesCorrectClasses(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient client = server.CreateClient(); + Assert.IsNotNull(configProvider, "Configuration Provider shouldn't be null after setting the configuration at runtime."); + Assert.IsTrue(configProvider.TryGetConfig(out RuntimeConfig configuration), "TryGetConfig should return true when the config is set."); + Assert.IsNotNull(configuration, "Config returned should not be null."); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + ConfigurationPostParameters expectedParameters = GetCosmosConfigurationParameters(); + Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, configuration.DataSource.DatabaseType, "Expected CosmosDB_NoSQL database type after configuring the runtime with CosmosDB_NoSQL settings."); + Assert.AreEqual(expectedParameters.Schema, configuration.DataSource.GetTypedOptions().GraphQLSchema, "Expected the schema in the configuration to match the one sent to the configuration endpoint."); - HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + // Don't use Assert.AreEqual, because a failure will print the entire connection string in the error message. + Assert.IsTrue(expectedParameters.ConnectionString == configuration.DataSource.ConnectionString, "Expected the connection string in the configuration to match the one sent to the configuration endpoint."); + string db = configuration.DataSource.GetTypedOptions().Database; + Assert.AreEqual(COSMOS_DATABASE_NAME, db, "Expected the database name in the runtime config to match the one sent to the configuration endpoint."); + } - ValidateCosmosDbSetup(server); - RuntimeConfigProvider configProvider = server.Services.GetService(); + [TestMethod("Validates that an exception is thrown if there's a null model in filter parser.")] + public void VerifyExceptionOnNullModelinFilterParser() + { + ODataParser parser = new(); + try + { + // FilterParser has no model so we expect exception + parser.GetFilterClause(filterQueryString: string.Empty, resourcePath: string.Empty); + Assert.Fail(); + } + catch (DataApiBuilderException exception) + { + Assert.AreEqual("The runtime has not been initialized with an Edm model.", exception.Message); + Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.UnexpectedError, exception.SubStatusCode); + } + } - Assert.IsNotNull(configProvider, "Configuration Provider shouldn't be null after setting the configuration at runtime."); - Assert.IsTrue(configProvider.TryGetConfig(out RuntimeConfig configuration), "TryGetConfig should return true when the config is set."); - Assert.IsNotNull(configuration, "Config returned should not be null."); + /// + /// This test reads the dab-config.MsSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of MsSql config file succeeds."), TestCategory(TestCategory.MSSQL)] + public Task TestReadingRuntimeConfigForMsSql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MSSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - ConfigurationPostParameters expectedParameters = GetCosmosConfigurationParameters(); - Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, configuration.DataSource.DatabaseType, "Expected CosmosDB_NoSQL database type after configuring the runtime with CosmosDB_NoSQL settings."); - Assert.AreEqual(expectedParameters.Schema, configuration.DataSource.GetTypedOptions().GraphQLSchema, "Expected the schema in the configuration to match the one sent to the configuration endpoint."); + /// + /// This test reads the dab-config.MySql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of MySql config file succeeds."), TestCategory(TestCategory.MYSQL)] + public Task TestReadingRuntimeConfigForMySql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MYSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - // Don't use Assert.AreEqual, because a failure will print the entire connection string in the error message. - Assert.IsTrue(expectedParameters.ConnectionString == configuration.DataSource.ConnectionString, "Expected the connection string in the configuration to match the one sent to the configuration endpoint."); - string db = configuration.DataSource.GetTypedOptions().Database; - Assert.AreEqual(COSMOS_DATABASE_NAME, db, "Expected the database name in the runtime config to match the one sent to the configuration endpoint."); - } + /// + /// This test reads the dab-config.PostgreSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of PostgreSql config file succeeds."), TestCategory(TestCategory.POSTGRESQL)] + public Task TestReadingRuntimeConfigForPostgreSql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{POSTGRESQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - [TestMethod("Validates that an exception is thrown if there's a null model in filter parser.")] - public void VerifyExceptionOnNullModelinFilterParser() - { - ODataParser parser = new(); - try - { - // FilterParser has no model so we expect exception - parser.GetFilterClause(filterQueryString: string.Empty, resourcePath: string.Empty); - Assert.Fail(); - } - catch (DataApiBuilderException exception) - { - Assert.AreEqual("The runtime has not been initialized with an Edm model.", exception.Message); - Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); - Assert.AreEqual(DataApiBuilderException.SubStatusCodes.UnexpectedError, exception.SubStatusCode); - } - } + /// + /// This test reads the dab-config.CosmosDb_NoSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of the CosmosDB_NoSQL config file succeeds."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public Task TestReadingRuntimeConfigForCosmos() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - /// - /// This test reads the dab-config.MsSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of MsSql config file succeeds."), TestCategory(TestCategory.MSSQL)] - public Task TestReadingRuntimeConfigForMsSql() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MSSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + /// + /// Helper method to validate the deserialization of the "entities" section of the config file + /// This is used in unit tests that validate the deserialization of the config files + /// + /// + private Task ConfigFileDeserializationValidationHelper(string jsonString) + { + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonString, out RuntimeConfig runtimeConfig), "Deserialization of the config file failed."); + return Verify(runtimeConfig); + } - /// - /// This test reads the dab-config.MySql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of MySql config file succeeds."), TestCategory(TestCategory.MYSQL)] - public Task TestReadingRuntimeConfigForMySql() + /// + /// This function verifies command line configuration provider takes higher + /// precedence than default configuration file dab-config.json + /// + [TestMethod("Validates command line configuration provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestCommandLineConfigurationProvider() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + string[] args = new[] { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MYSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + $"--ConfigFileName={RuntimeConfigLoader.CONFIGFILE_NAME}." + + $"{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}" + }; - /// - /// This test reads the dab-config.PostgreSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of PostgreSql config file succeeds."), TestCategory(TestCategory.POSTGRESQL)] - public Task TestReadingRuntimeConfigForPostgreSql() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{POSTGRESQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + TestServer server = new(Program.CreateWebHostBuilder(args)); - /// - /// This test reads the dab-config.CosmosDb_NoSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of the CosmosDB_NoSQL config file succeeds."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public Task TestReadingRuntimeConfigForCosmos() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + ValidateCosmosDbSetup(server); + } - /// - /// Helper method to validate the deserialization of the "entities" section of the config file - /// This is used in unit tests that validate the deserialization of the config files - /// - /// - private Task ConfigFileDeserializationValidationHelper(string jsonString) - { - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonString, out RuntimeConfig runtimeConfig), "Deserialization of the config file failed."); - return Verify(runtimeConfig); - } + /// + /// This function verifies the environment variable DAB_ENVIRONMENT + /// takes precedence than ASPNETCORE_ENVIRONMENT for the configuration file. + /// + [TestMethod("Validates precedence is given to DAB_ENVIRONMENT environment variable name."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestRuntimeEnvironmentVariable() + { + Environment.SetEnvironmentVariable( + ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + Environment.SetEnvironmentVariable( + RuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - /// - /// This function verifies command line configuration provider takes higher - /// precedence than default configuration file dab-config.json - /// - [TestMethod("Validates command line configuration provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestCommandLineConfigurationProvider() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - string[] args = new[] - { - $"--ConfigFileName={RuntimeConfigLoader.CONFIGFILE_NAME}." + - $"{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}" - }; + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - TestServer server = new(Program.CreateWebHostBuilder(args)); + ValidateCosmosDbSetup(server); + } - ValidateCosmosDbSetup(server); - } + [TestMethod("Validates the runtime configuration file."), TestCategory(TestCategory.MSSQL)] + public void TestConfigIsValid() + { + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + RuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); + RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configPath); + + Mock> configValidatorLogger = new(); + IConfigValidator configValidator = + new RuntimeConfigValidator( + configProvider, + new MockFileSystem(), + configValidatorLogger.Object); + + configValidator.ValidateConfig(); + TestHelper.UnsetAllDABEnvironmentVariables(); + } - /// - /// This function verifies the environment variable DAB_ENVIRONMENT - /// takes precedence than ASPNETCORE_ENVIRONMENT for the configuration file. - /// - [TestMethod("Validates precedence is given to DAB_ENVIRONMENT environment variable name."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestRuntimeEnvironmentVariable() - { - Environment.SetEnvironmentVariable( - ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - Environment.SetEnvironmentVariable( - RuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + /// + /// Set the connection string to an invalid value and expect the service to be unavailable + /// since without this env var, it would be available - guaranteeing this env variable + /// has highest precedence irrespective of what the connection string is in the config file. + /// Verifying the Exception thrown. + /// + [TestMethod($"Validates that environment variable {RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} has highest precedence."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestConnectionStringEnvVarHasHighestPrecedence() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + Environment.SetEnvironmentVariable( + RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING, + "Invalid Connection String"); + try + { TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - - ValidateCosmosDbSetup(server); + _ = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.Fail($"{RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} is not given highest precedence"); } - - [TestMethod("Validates the runtime configuration file."), TestCategory(TestCategory.MSSQL)] - public void TestConfigIsValid() + catch (Exception e) { - TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); - RuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); - RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configPath); - - Mock> configValidatorLogger = new(); - IConfigValidator configValidator = - new RuntimeConfigValidator( - configProvider, - new MockFileSystem(), - configValidatorLogger.Object); - - configValidator.ValidateConfig(); - TestHelper.UnsetAllDABEnvironmentVariables(); + Assert.AreEqual(typeof(ApplicationException), e.GetType()); + Assert.AreEqual( + $"Could not initialize the engine with the runtime config file: " + + $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}", + e.Message); } + } - /// - /// Set the connection string to an invalid value and expect the service to be unavailable - /// since without this env var, it would be available - guaranteeing this env variable - /// has highest precedence irrespective of what the connection string is in the config file. - /// Verifying the Exception thrown. - /// - [TestMethod($"Validates that environment variable {RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} has highest precedence."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestConnectionStringEnvVarHasHighestPrecedence() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - Environment.SetEnvironmentVariable( - RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING, - "Invalid Connection String"); + /// + /// Test to verify the precedence logic for config file based on Environment variables. + /// + [DataTestMethod] + [DataRow("HostTest", "Test", false, $"{CONFIGFILE_NAME}.Test{CONFIG_EXTENSION}", DisplayName = "hosting and dab environment set, without considering overrides.")] + [DataRow("HostTest", "", false, $"{CONFIGFILE_NAME}.HostTest{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, without considering overrides.")] + [DataRow("", "Test1", false, $"{CONFIGFILE_NAME}.Test1{CONFIG_EXTENSION}", DisplayName = "only dab environment set, without considering overrides.")] + [DataRow("", "Test2", true, $"{CONFIGFILE_NAME}.Test2.overrides{CONFIG_EXTENSION}", DisplayName = "only dab environment set, considering overrides.")] + [DataRow("HostTest1", "", true, $"{CONFIGFILE_NAME}.HostTest1.overrides{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, considering overrides.")] + public void TestGetConfigFileNameForEnvironment( + string hostingEnvironmentValue, + string environmentValue, + bool considerOverrides, + string expectedRuntimeConfigFile) + { + MockFileSystem fileSystem = new(); + fileSystem.AddFile(expectedRuntimeConfigFile, new MockFileData(string.Empty)); + RuntimeConfigLoader runtimeConfigLoader = new(fileSystem); + + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, hostingEnvironmentValue); + Environment.SetEnvironmentVariable(RUNTIME_ENVIRONMENT_VAR_NAME, environmentValue); + string actualRuntimeConfigFile = runtimeConfigLoader.GetFileNameForEnvironment(hostingEnvironmentValue, considerOverrides); + Assert.AreEqual(expectedRuntimeConfigFile, actualRuntimeConfigFile); + } - try - { - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - _ = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.Fail($"{RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} is not given highest precedence"); - } - catch (Exception e) + /// + /// Test different graphql endpoints in different host modes + /// when accessed interactively via browser. + /// + /// The endpoint route + /// The mode in which the service is executing. + /// Expected Status Code. + /// The expected phrase in the response body. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop", + DisplayName = "GraphQL endpoint with no query in development mode.")] + [DataRow("/graphql", HostMode.Production, HttpStatusCode.BadRequest, + "Either the parameter query or the parameter id has to be set", + DisplayName = "GraphQL endpoint with no query in production mode.")] + [DataRow("/graphql/ui", HostMode.Development, HttpStatusCode.NotFound, + DisplayName = "Default BananaCakePop in development mode.")] + [DataRow("/graphql/ui", HostMode.Production, HttpStatusCode.NotFound, + DisplayName = "Default BananaCakePop in production mode.")] + [DataRow("/graphql?query={book_by_pk(id: 1){title}}", + HostMode.Development, HttpStatusCode.Moved, + DisplayName = "GraphQL endpoint with query in development mode.")] + [DataRow("/graphql?query={book_by_pk(id: 1){title}}", + HostMode.Production, HttpStatusCode.OK, "data", + DisplayName = "GraphQL endpoint with query in production mode.")] + [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Development, HttpStatusCode.BadRequest, + "GraphQL request redirected to favicon.ico.", + DisplayName = "Redirected endpoint in development mode.")] + [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Production, HttpStatusCode.BadRequest, + "GraphQL request redirected to favicon.ico.", + DisplayName = "Redirected endpoint in production mode.")] + public async Task TestInteractiveGraphQLEndpoints( + string endpoint, + HostMode HostMode, + HttpStatusCode expectedStatusCode, + string expectedContent = "") + { + const string CUSTOM_CONFIG = "custom-config.json"; + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + FileSystem fileSystem = new(); + RuntimeConfigLoader loader = new(fileSystem); + loader.TryLoadKnownConfig(out RuntimeConfig config); + + RuntimeConfig configWithCustomHostMode = config with + { + Runtime = config.Runtime with { - Assert.AreEqual(typeof(ApplicationException), e.GetType()); - Assert.AreEqual( - $"Could not initialize the engine with the runtime config file: " + - $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}", - e.Message); + Host = config.Runtime.Host with { Mode = HostMode } } - } - - /// - /// Test to verify the precedence logic for config file based on Environment variables. - /// - [DataTestMethod] - [DataRow("HostTest", "Test", false, $"{CONFIGFILE_NAME}.Test{CONFIG_EXTENSION}", DisplayName = "hosting and dab environment set, without considering overrides.")] - [DataRow("HostTest", "", false, $"{CONFIGFILE_NAME}.HostTest{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, without considering overrides.")] - [DataRow("", "Test1", false, $"{CONFIGFILE_NAME}.Test1{CONFIG_EXTENSION}", DisplayName = "only dab environment set, without considering overrides.")] - [DataRow("", "Test2", true, $"{CONFIGFILE_NAME}.Test2.overrides{CONFIG_EXTENSION}", DisplayName = "only dab environment set, considering overrides.")] - [DataRow("HostTest1", "", true, $"{CONFIGFILE_NAME}.HostTest1.overrides{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, considering overrides.")] - public void TestGetConfigFileNameForEnvironment( - string hostingEnvironmentValue, - string environmentValue, - bool considerOverrides, - string expectedRuntimeConfigFile) + }; + File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); + string[] args = new[] { - MockFileSystem fileSystem = new(); - fileSystem.AddFile(expectedRuntimeConfigFile, new MockFileData(string.Empty)); - RuntimeConfigLoader runtimeConfigLoader = new(fileSystem); - - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, hostingEnvironmentValue); - Environment.SetEnvironmentVariable(RUNTIME_ENVIRONMENT_VAR_NAME, environmentValue); - string actualRuntimeConfigFile = runtimeConfigLoader.GetFileNameForEnvironment(hostingEnvironmentValue, considerOverrides); - Assert.AreEqual(expectedRuntimeConfigFile, actualRuntimeConfigFile); - } + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - /// - /// Test different graphql endpoints in different host modes - /// when accessed interactively via browser. - /// - /// The endpoint route - /// The mode in which the service is executing. - /// Expected Status Code. - /// The expected phrase in the response body. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop", - DisplayName = "GraphQL endpoint with no query in development mode.")] - [DataRow("/graphql", HostMode.Production, HttpStatusCode.BadRequest, - "Either the parameter query or the parameter id has to be set", - DisplayName = "GraphQL endpoint with no query in production mode.")] - [DataRow("/graphql/ui", HostMode.Development, HttpStatusCode.NotFound, - DisplayName = "Default BananaCakePop in development mode.")] - [DataRow("/graphql/ui", HostMode.Production, HttpStatusCode.NotFound, - DisplayName = "Default BananaCakePop in production mode.")] - [DataRow("/graphql?query={book_by_pk(id: 1){title}}", - HostMode.Development, HttpStatusCode.Moved, - DisplayName = "GraphQL endpoint with query in development mode.")] - [DataRow("/graphql?query={book_by_pk(id: 1){title}}", - HostMode.Production, HttpStatusCode.OK, "data", - DisplayName = "GraphQL endpoint with query in production mode.")] - [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Development, HttpStatusCode.BadRequest, - "GraphQL request redirected to favicon.ico.", - DisplayName = "Redirected endpoint in development mode.")] - [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Production, HttpStatusCode.BadRequest, - "GraphQL request redirected to favicon.ico.", - DisplayName = "Redirected endpoint in production mode.")] - public async Task TestInteractiveGraphQLEndpoints( - string endpoint, - HostMode HostMode, - HttpStatusCode expectedStatusCode, - string expectedContent = "") + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); { - const string CUSTOM_CONFIG = "custom-config.json"; - TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); - FileSystem fileSystem = new(); - RuntimeConfigLoader loader = new(fileSystem); - loader.TryLoadKnownConfig(out RuntimeConfig config); + HttpRequestMessage request = new(HttpMethod.Get, endpoint); - RuntimeConfig configWithCustomHostMode = config with - { - Runtime = config.Runtime with - { - Host = config.Runtime.Host with { Mode = HostMode } - } - }; - File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; - - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - { - HttpRequestMessage request = new(HttpMethod.Get, endpoint); + // Adding the following headers simulates an interactive browser request. + request.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); + request.Headers.Add("accept", BROWSER_ACCEPT_HEADER); - // Adding the following headers simulates an interactive browser request. - request.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); - request.Headers.Add("accept", BROWSER_ACCEPT_HEADER); - - HttpResponseMessage response = await client.SendAsync(request); - Assert.AreEqual(expectedStatusCode, response.StatusCode); - string actualBody = await response.Content.ReadAsStringAsync(); - Assert.IsTrue(actualBody.Contains(expectedContent)); + HttpResponseMessage response = await client.SendAsync(request); + Assert.AreEqual(expectedStatusCode, response.StatusCode); + string actualBody = await response.Content.ReadAsStringAsync(); + Assert.IsTrue(actualBody.Contains(expectedContent)); - TestHelper.UnsetAllDABEnvironmentVariables(); - } + TestHelper.UnsetAllDABEnvironmentVariables(); } + } - /// - /// Tests that the custom path rewriting middleware properly rewrites the - /// first segment of a path (/segment1/.../segmentN) when the segment matches - /// the custom configured GraphQLEndpoint. - /// Note: The GraphQL service is always internally mapped to /graphql - /// - /// The custom configured GraphQL path in configuration - /// The path used in the web request executed in the test. - /// Expected Http success/error code - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/graphql", "/gql", HttpStatusCode.BadRequest, DisplayName = "Request to non-configured graphQL endpoint is handled by REST controller.")] - [DataRow("/graphql", "/graphql", HttpStatusCode.OK, DisplayName = "Request to configured default GraphQL endpoint succeeds, path not rewritten.")] - [DataRow("/gql", "/gql/additionalURLsegment", HttpStatusCode.OK, DisplayName = "GraphQL request path (with extra segments) rewritten to match internally set GraphQL endpoint /graphql.")] - [DataRow("/gql", "/gql", HttpStatusCode.OK, DisplayName = "GraphQL request path rewritten to match internally set GraphQL endpoint /graphql.")] - [DataRow("/gql", "/api/book", HttpStatusCode.NotFound, DisplayName = "Non-GraphQL request's path is not rewritten and is handled by REST controller.")] - [DataRow("/gql", "/graphql", HttpStatusCode.NotFound, DisplayName = "Requests to default/internally set graphQL endpoint fail when configured endpoint differs.")] - public async Task TestPathRewriteMiddlewareForGraphQL( - string graphQLConfiguredPath, - string requestPath, - HttpStatusCode expectedStatusCode) - { - GraphQLRuntimeOptions graphqlOptions = new(Path: graphQLConfiguredPath); + /// + /// Tests that the custom path rewriting middleware properly rewrites the + /// first segment of a path (/segment1/.../segmentN) when the segment matches + /// the custom configured GraphQLEndpoint. + /// Note: The GraphQL service is always internally mapped to /graphql + /// + /// The custom configured GraphQL path in configuration + /// The path used in the web request executed in the test. + /// Expected Http success/error code + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/graphql", "/gql", HttpStatusCode.BadRequest, DisplayName = "Request to non-configured graphQL endpoint is handled by REST controller.")] + [DataRow("/graphql", "/graphql", HttpStatusCode.OK, DisplayName = "Request to configured default GraphQL endpoint succeeds, path not rewritten.")] + [DataRow("/gql", "/gql/additionalURLsegment", HttpStatusCode.OK, DisplayName = "GraphQL request path (with extra segments) rewritten to match internally set GraphQL endpoint /graphql.")] + [DataRow("/gql", "/gql", HttpStatusCode.OK, DisplayName = "GraphQL request path rewritten to match internally set GraphQL endpoint /graphql.")] + [DataRow("/gql", "/api/book", HttpStatusCode.NotFound, DisplayName = "Non-GraphQL request's path is not rewritten and is handled by REST controller.")] + [DataRow("/gql", "/graphql", HttpStatusCode.NotFound, DisplayName = "Requests to default/internally set graphQL endpoint fail when configured endpoint differs.")] + public async Task TestPathRewriteMiddlewareForGraphQL( + string graphQLConfiguredPath, + string requestPath, + HttpStatusCode expectedStatusCode) + { + GraphQLRuntimeOptions graphqlOptions = new(Path: graphQLConfiguredPath); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new()); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new()); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" }; + string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - string query = @"{ + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + string query = @"{ book_by_pk(id: 1) { id, title, @@ -839,120 +839,120 @@ public async Task TestPathRewriteMiddlewareForGraphQL( } }"; - var payload = new { query }; + var payload = new { query }; - HttpRequestMessage request = new(HttpMethod.Post, requestPath) - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage request = new(HttpMethod.Post, requestPath) + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage response = await client.SendAsync(request); - string body = await response.Content.ReadAsStringAsync(); + HttpResponseMessage response = await client.SendAsync(request); + string body = await response.Content.ReadAsStringAsync(); - Assert.AreEqual(expectedStatusCode, response.StatusCode); - } + Assert.AreEqual(expectedStatusCode, response.StatusCode); + } - /// - /// Validates the error message that is returned for REST requests with incorrect parameter type - /// when the engine is running in Production mode. The error messages in Production mode is - /// very generic to not reveal information about the underlying database objects backing the entity. - /// This test runs against a MsSql database. However, generic error messages will be returned in Production - /// mode when run against PostgreSql and MySql databases. - /// - /// Type of REST request - /// Endpoint for the REST request - /// Right error message that should be shown to the end user - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(SupportedHttpVerb.Get, "/api/Book/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/books_view_all/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a view in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/GetBook?id=one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request on a stored-procedure with incorrect parameter type in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/GQLmappings/column1/one", null, "Invalid value provided for field: column1", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type with alias defined for primary key column on a table in production mode")] - [DataRow(SupportedHttpVerb.Post, "/api/Book", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a POST request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Put, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PUT request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Put, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a bad PUT request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PATCH request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a PATCH request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Delete, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a DELETE request with incorrect primary key parameter type on a table in production mode")] - public async Task TestGenericErrorMessageForRestApiInProductionMode( - SupportedHttpVerb requestType, - string requestPath, - string requestBody, - string expectedErrorMessage) + /// + /// Validates the error message that is returned for REST requests with incorrect parameter type + /// when the engine is running in Production mode. The error messages in Production mode is + /// very generic to not reveal information about the underlying database objects backing the entity. + /// This test runs against a MsSql database. However, generic error messages will be returned in Production + /// mode when run against PostgreSql and MySql databases. + /// + /// Type of REST request + /// Endpoint for the REST request + /// Right error message that should be shown to the end user + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(SupportedHttpVerb.Get, "/api/Book/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/books_view_all/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a view in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/GetBook?id=one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request on a stored-procedure with incorrect parameter type in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/GQLmappings/column1/one", null, "Invalid value provided for field: column1", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type with alias defined for primary key column on a table in production mode")] + [DataRow(SupportedHttpVerb.Post, "/api/Book", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a POST request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Put, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PUT request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Put, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a bad PUT request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PATCH request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a PATCH request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Delete, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a DELETE request with incorrect primary key parameter type on a table in production mode")] + public async Task TestGenericErrorMessageForRestApiInProductionMode( + SupportedHttpVerb requestType, + string requestPath, + string requestBody, + string expectedErrorMessage) + { + const string CUSTOM_CONFIG = "custom-config.json"; + TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL); + string[] args = new[] { - const string CUSTOM_CONFIG = "custom-config.json"; - TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(requestType); + HttpRequestMessage request; + if (requestType is SupportedHttpVerb.Get || requestType is SupportedHttpVerb.Delete) { - HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(requestType); - HttpRequestMessage request; - if (requestType is SupportedHttpVerb.Get || requestType is SupportedHttpVerb.Delete) - { - request = new(httpMethod, requestPath); - } - else + request = new(httpMethod, requestPath); + } + else + { + request = new(httpMethod, requestPath) { - request = new(httpMethod, requestPath) - { - Content = JsonContent.Create(requestBody) - }; - } - - HttpResponseMessage response = await client.SendAsync(request); - string body = await response.Content.ReadAsStringAsync(); - Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); - Assert.IsTrue(body.Contains(expectedErrorMessage)); + Content = JsonContent.Create(requestBody) + }; } + + HttpResponseMessage response = await client.SendAsync(request); + string body = await response.Content.ReadAsStringAsync(); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + Assert.IsTrue(body.Contains(expectedErrorMessage)); } + } - /// - /// Tests that the when Rest or GraphQL is disabled Globally, - /// any requests made will get a 404 response. - /// - /// The custom configured REST enabled property in configuration. - /// The custom configured GraphQL enabled property in configuration. - /// Expected HTTP status code code for the Rest request - /// Expected HTTP status code code for the GraphQL request - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] - public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( - bool isRestEnabled, - bool isGraphQLEnabled, - HttpStatusCode expectedStatusCodeForREST, - HttpStatusCode expectedStatusCodeForGraphQL, - string configurationEndpoint) - { - GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); - RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); + /// + /// Tests that the when Rest or GraphQL is disabled Globally, + /// any requests made will get a 404 response. + /// + /// The custom configured REST enabled property in configuration. + /// The custom configured GraphQL enabled property in configuration. + /// Expected HTTP status code code for the Rest request + /// Expected HTTP status code code for the GraphQL request + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] + [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] + [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] + [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] + [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] + [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] + public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( + bool isRestEnabled, + bool isGraphQLEnabled, + HttpStatusCode expectedStatusCodeForREST, + HttpStatusCode expectedStatusCodeForGraphQL, + string configurationEndpoint) + { + GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); + RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - // Non-Hosted Scenario - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - string query = @"{ + // Non-Hosted Scenario + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + string query = @"{ book_by_pk(id: 1) { id, title, @@ -960,78 +960,78 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir } }"; - object payload = new { query }; + object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); + HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); + Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); - HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); - } + HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); + HttpResponseMessage restResponse = await client.SendAsync(restRequest); + Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); + } - // Hosted Scenario - // Instantiate new server with no runtime config for post-startup configuration hydration tests. - using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) - using (HttpClient client = server.CreateClient()) - { - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + // Hosted Scenario + // Instantiate new server with no runtime config for post-startup configuration hydration tests. + using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) + using (HttpClient client = server.CreateClient()) + { + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpResponseMessage postResult = - await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + HttpResponseMessage postResult = + await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); + HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); - Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); + Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); - HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); + HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); - Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); + Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); - } } + } - /// - /// Engine supports config with some views that do not have keyfields specified in the config for MsSQL. - /// This Test validates that support. It creates a custom config with a view and no keyfields specified. - /// It checks both Rest and GraphQL queries are tested to return Success. - /// - [TestMethod, TestCategory(TestCategory.MSSQL)] - public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() - { - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - Entity viewEntity = new( - Source: new("books_view_all", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - GraphQL: new("", ""), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null - ); + /// + /// Engine supports config with some views that do not have keyfields specified in the config for MsSQL. + /// This Test validates that support. It creates a custom config with a view and no keyfields specified. + /// It checks both Rest and GraphQL queries are tested to return Success. + /// + [TestMethod, TestCategory(TestCategory.MSSQL)] + public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() + { + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + Entity viewEntity = new( + Source: new("books_view_all", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + GraphQL: new("", ""), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null + ); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), viewEntity, "books_view_all"); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), viewEntity, "books_view_all"); - const string CUSTOM_CONFIG = "custom-config.json"; + const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText( - CUSTOM_CONFIG, - configuration.ToJson()); + File.WriteAllText( + CUSTOM_CONFIG, + configuration.ToJson()); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - string query = @"{ + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + string query = @"{ books_view_alls { items{ id @@ -1040,465 +1040,465 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() } }"; - object payload = new { query }; + object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode); - string body = await graphQLResponse.Content.ReadAsStringAsync(); - Assert.IsFalse(body.Contains("errors")); // In GraphQL, All errors end up in the errors array, no matter what kind of error they are. + HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); + Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode); + string body = await graphQLResponse.Content.ReadAsStringAsync(); + Assert.IsFalse(body.Contains("errors")); // In GraphQL, All errors end up in the errors array, no matter what kind of error they are. - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/books_view_all"); - HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode); - } + HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/books_view_all"); + HttpResponseMessage restResponse = await client.SendAsync(restRequest); + Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode); } + } - /// - /// Tests that Startup.cs properly handles EasyAuth authentication configuration. - /// AppService as Identity Provider while in Production mode will result in startup error. - /// An Azure AppService environment has environment variables on the host which indicate - /// the environment is, in fact, an AppService environment. - /// - /// HostMode in Runtime config - Development or Production. - /// EasyAuth auth type - AppService or StaticWebApps. - /// Whether to set the AppService host environment variables. - /// Whether an error is expected. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] - public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError) + /// + /// Tests that Startup.cs properly handles EasyAuth authentication configuration. + /// AppService as Identity Provider while in Production mode will result in startup error. + /// An Azure AppService environment has environment variables on the host which indicate + /// the environment is, in fact, an AppService environment. + /// + /// HostMode in Runtime config - Development or Production. + /// EasyAuth auth type - AppService or StaticWebApps. + /// Whether to set the AppService host environment variables. + /// Whether an error is expected. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] + public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError) + { + // Clears or sets App Service Environment Variables based on test input. + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null); + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null); + TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); + + FileSystem fileSystem = new(); + RuntimeConfigLoader loader = new(fileSystem); + + RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(loader); + RuntimeConfig config = configProvider.GetConfig(); + + // Setup configuration + AuthenticationOptions AuthenticationOptions = new(Provider: authType.ToString(), null); + RuntimeOptions runtimeOptions = new( + Rest: new(), + GraphQL: new(), + Host: new(null, AuthenticationOptions, hostMode) + ); + RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; + + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); + string[] args = new[] { - // Clears or sets App Service Environment Variables based on test input. - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null); - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null); - TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); - - FileSystem fileSystem = new(); - RuntimeConfigLoader loader = new(fileSystem); - - RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(loader); - RuntimeConfig config = configProvider.GetConfig(); - - // Setup configuration - AuthenticationOptions AuthenticationOptions = new(Provider: authType.ToString(), null); - RuntimeOptions runtimeOptions = new( - Rest: new(), - GraphQL: new(), - Host: new(null, AuthenticationOptions, hostMode) - ); - RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; - - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - // This test only checks for startup errors, so no requests are sent to the test server. - try - { - using TestServer server = new(Program.CreateWebHostBuilder(args)); - Assert.IsFalse(expectError, message: "Expected error faulting AppService config in production mode."); - } - catch (DataApiBuilderException ex) - { - Assert.IsTrue(expectError, message: ex.Message); - Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); - } + // This test only checks for startup errors, so no requests are sent to the test server. + try + { + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(expectError, message: "Expected error faulting AppService config in production mode."); } - - /// - /// Integration test that validates schema introspection requests fail - /// when allow-introspection is false in the runtime configuration. - /// TestCategory is required for CI/CD pipeline to inject a connection string. - /// - /// - [TestCategory(TestCategory.MSSQL)] - [DataTestMethod] - [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT, DisplayName = "Disabled introspection returns GraphQL error.")] - [DataRow(true, false, null, CONFIGURATION_ENDPOINT, DisplayName = "Enabled introspection does not return introspection forbidden error.")] - [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT_V2, DisplayName = "Disabled introspection returns GraphQL error.")] - [DataRow(true, false, null, CONFIGURATION_ENDPOINT_V2, DisplayName = "Enabled introspection does not return introspection forbidden error.")] - public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool expectError, string errorMessage, string configurationEndpoint) + catch (DataApiBuilderException ex) { - GraphQLRuntimeOptions graphqlOptions = new(AllowIntrospection: enableIntrospection); - RestRuntimeOptions restRuntimeOptions = new(); - - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + Assert.IsTrue(expectError, message: ex.Message); + Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); + } + } - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + /// + /// Integration test that validates schema introspection requests fail + /// when allow-introspection is false in the runtime configuration. + /// TestCategory is required for CI/CD pipeline to inject a connection string. + /// + /// + [TestCategory(TestCategory.MSSQL)] + [DataTestMethod] + [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT, DisplayName = "Disabled introspection returns GraphQL error.")] + [DataRow(true, false, null, CONFIGURATION_ENDPOINT, DisplayName = "Enabled introspection does not return introspection forbidden error.")] + [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT_V2, DisplayName = "Disabled introspection returns GraphQL error.")] + [DataRow(true, false, null, CONFIGURATION_ENDPOINT_V2, DisplayName = "Enabled introspection does not return introspection forbidden error.")] + public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool expectError, string errorMessage, string configurationEndpoint) + { + GraphQLRuntimeOptions graphqlOptions = new(AllowIntrospection: enableIntrospection); + RestRuntimeOptions restRuntimeOptions = new(); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - await ExecuteGraphQLIntrospectionQueries(server, client, expectError); - } + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - // Instantiate new server with no runtime config for post-startup configuration hydration tests. - using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) - using (HttpClient client = server.CreateClient()) - { - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); - - Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - await ExecuteGraphQLIntrospectionQueries(server, client, expectError); - } + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + await ExecuteGraphQLIntrospectionQueries(server, client, expectError); } - /// - /// Indirectly tests IsGraphQLReservedName(). Runtime config provided to engine which will - /// trigger SqlMetadataProvider PopulateSourceDefinitionAsync() to pull column metadata from - /// the table "graphql_incompatible." That table contains columns which collide with reserved GraphQL - /// introspection field names which begin with double underscore (__). - /// - [TestCategory(TestCategory.MSSQL)] - [DataTestMethod] - [DataRow(true, true, "__typeName", "__introspectionField", true, DisplayName = "Name violation, fails since no proper mapping set.")] - [DataRow(true, true, "__typeName", "columnMapping", false, DisplayName = "Name violation, but OK since proper mapping set.")] - [DataRow(false, true, null, null, false, DisplayName = "Name violation, but OK since GraphQL globally disabled.")] - [DataRow(true, false, null, null, false, DisplayName = "Name violation, but OK since GraphQL disabled for entity.")] - public void TestInvalidDatabaseColumnNameHandling( - bool globalGraphQLEnabled, - bool entityGraphQLEnabled, - string columnName, - string columnMapping, - bool expectError) + // Instantiate new server with no runtime config for post-startup configuration hydration tests. + using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) + using (HttpClient client = server.CreateClient()) { - GraphQLRuntimeOptions graphqlOptions = new(Enabled: globalGraphQLEnabled); - RestRuntimeOptions restRuntimeOptions = new(Enabled: true); - - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - - // Configure Entity for testing - Dictionary mappings = new() - { - { "__introspectionName", "conformingIntrospectionName" } - }; - - if (!string.IsNullOrWhiteSpace(columnMapping)) - { - mappings.Add(columnName, columnMapping); - } - - Entity entity = new( - Source: new("graphql_incompatible", EntitySourceType.Table, null, null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: mappings - ); + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, "graphqlNameCompat"); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; - - try - { - using TestServer server = new(Program.CreateWebHostBuilder(args)); - Assert.IsFalse(expectError, message: "Expected startup to fail."); - } - catch (Exception ex) - { - Assert.IsTrue(expectError, message: "Startup was not expected to fail. " + ex.Message); - } + await ExecuteGraphQLIntrospectionQueries(server, client, expectError); } + } - /// - /// Test different Swagger endpoints in different host modes when accessed interactively via browser. - /// Two pass request scheme: - /// 1 - Send get request to expected Swagger endpoint /swagger - /// Response - Internally Swagger sends HTTP 301 Moved Permanently with Location header - /// pointing to exact Swagger page (/swagger/index.html) - /// 2 - Send GET request to path referred to by Location header in previous response - /// Response - Successful loading of SwaggerUI HTML, with reference to endpoint used - /// to retrieve OpenAPI document. This test ensures that Swagger components load, but - /// does not confirm that a proper OpenAPI document was created. - /// - /// The custom REST route - /// The mode in which the service is executing. - /// Whether to expect an error. - /// Expected Status Code. - /// Snippet of expected HTML to be emitted from successful page load. - /// This should note the openapi route that Swagger will use to retrieve the OpenAPI document. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/api", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/api/openapi\"", DisplayName = "SwaggerUI enabled in development mode.")] - [DataRow("/custompath", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/custompath/openapi\"", DisplayName = "SwaggerUI enabled with custom REST path in development mode.")] - [DataRow("/api", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode.")] - [DataRow("/custompath", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode with custom REST path.")] - public async Task OpenApi_InteractiveSwaggerUI( - string customRestPath, - HostMode hostModeType, - bool expectsError, - HttpStatusCode expectedStatusCode, - string expectedOpenApiTargetContent) - { - string swaggerEndpoint = "/swagger"; - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + /// + /// Indirectly tests IsGraphQLReservedName(). Runtime config provided to engine which will + /// trigger SqlMetadataProvider PopulateSourceDefinitionAsync() to pull column metadata from + /// the table "graphql_incompatible." That table contains columns which collide with reserved GraphQL + /// introspection field names which begin with double underscore (__). + /// + [TestCategory(TestCategory.MSSQL)] + [DataTestMethod] + [DataRow(true, true, "__typeName", "__introspectionField", true, DisplayName = "Name violation, fails since no proper mapping set.")] + [DataRow(true, true, "__typeName", "columnMapping", false, DisplayName = "Name violation, but OK since proper mapping set.")] + [DataRow(false, true, null, null, false, DisplayName = "Name violation, but OK since GraphQL globally disabled.")] + [DataRow(true, false, null, null, false, DisplayName = "Name violation, but OK since GraphQL disabled for entity.")] + public void TestInvalidDatabaseColumnNameHandling( + bool globalGraphQLEnabled, + bool entityGraphQLEnabled, + string columnName, + string columnMapping, + bool expectError) + { + GraphQLRuntimeOptions graphqlOptions = new(Enabled: globalGraphQLEnabled); + RestRuntimeOptions restRuntimeOptions = new(Enabled: true); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource: dataSource, new(), new(Path: customRestPath)); - configuration = configuration - with - { - Runtime = configuration.Runtime - with - { - Host = configuration.Runtime.Host - with - { Mode = hostModeType } - } - }; - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText( - CUSTOM_CONFIG, - configuration.ToJson()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG}" - }; + // Configure Entity for testing + Dictionary mappings = new() + { + { "__introspectionName", "conformingIntrospectionName" } + }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - HttpRequestMessage initialRequest = new(HttpMethod.Get, swaggerEndpoint); + if (!string.IsNullOrWhiteSpace(columnMapping)) + { + mappings.Add(columnName, columnMapping); + } - // Adding the following headers simulates an interactive browser request. - initialRequest.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); - initialRequest.Headers.Add("accept", BROWSER_ACCEPT_HEADER); + Entity entity = new( + Source: new("graphql_incompatible", EntitySourceType.Table, null, null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: mappings + ); - HttpResponseMessage response = await client.SendAsync(initialRequest); - if (expectsError) - { - // Redirect(HTTP 301) and follow up request to the returned path - // do not occur in a failure scenario. Only HTTP 400 (Bad Request) - // is expected. - Assert.AreEqual(expectedStatusCode, response.StatusCode); - } - else - { - // Swagger endpoint internally configured to reroute from /swagger to /swagger/index.html - Assert.AreEqual(HttpStatusCode.MovedPermanently, response.StatusCode); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, "graphqlNameCompat"); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - HttpRequestMessage followUpRequest = new(HttpMethod.Get, response.Headers.Location); - HttpResponseMessage followUpResponse = await client.SendAsync(followUpRequest); - Assert.AreEqual(expectedStatusCode, followUpResponse.StatusCode); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - // Validate that Swagger requests OpenAPI document using REST path defined in runtime config. - string actualBody = await followUpResponse.Content.ReadAsStringAsync(); - Assert.AreEqual(true, actualBody.Contains(expectedOpenApiTargetContent)); - } - } + try + { + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(expectError, message: "Expected startup to fail."); + } + catch (Exception ex) + { + Assert.IsTrue(expectError, message: "Startup was not expected to fail. " + ex.Message); } + } + + /// + /// Test different Swagger endpoints in different host modes when accessed interactively via browser. + /// Two pass request scheme: + /// 1 - Send get request to expected Swagger endpoint /swagger + /// Response - Internally Swagger sends HTTP 301 Moved Permanently with Location header + /// pointing to exact Swagger page (/swagger/index.html) + /// 2 - Send GET request to path referred to by Location header in previous response + /// Response - Successful loading of SwaggerUI HTML, with reference to endpoint used + /// to retrieve OpenAPI document. This test ensures that Swagger components load, but + /// does not confirm that a proper OpenAPI document was created. + /// + /// The custom REST route + /// The mode in which the service is executing. + /// Whether to expect an error. + /// Expected Status Code. + /// Snippet of expected HTML to be emitted from successful page load. + /// This should note the openapi route that Swagger will use to retrieve the OpenAPI document. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/api", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/api/openapi\"", DisplayName = "SwaggerUI enabled in development mode.")] + [DataRow("/custompath", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/custompath/openapi\"", DisplayName = "SwaggerUI enabled with custom REST path in development mode.")] + [DataRow("/api", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode.")] + [DataRow("/custompath", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode with custom REST path.")] + public async Task OpenApi_InteractiveSwaggerUI( + string customRestPath, + HostMode hostModeType, + bool expectsError, + HttpStatusCode expectedStatusCode, + string expectedOpenApiTargetContent) + { + string swaggerEndpoint = "/swagger"; + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - /// - /// Validates the OpenAPI documentor behavior when enabling and disabling the global REST endpoint - /// for the DAB engine. - /// Global REST enabled: - /// - GET to /openapi returns the created OpenAPI document and succeeds with 200 OK. - /// Global REST disabled: - /// - GET to /openapi fails with 404 Not Found. - /// - [DataTestMethod] - [DataRow(true, false, DisplayName = "Global REST endpoint enabled - successful OpenAPI doc retrieval")] - [DataRow(false, true, DisplayName = "Global REST endpoint disabled - OpenAPI doc does not exist - HTTP404 NotFound.")] - [TestCategory(TestCategory.MSSQL)] - public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expectsError) + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource: dataSource, new(), new(Path: customRestPath)); + configuration = configuration + with { - // At least one entity is required in the runtime config for the engine to start. - // Even though this entity is not under test, it must be supplied to the config - // file creation function. - Entity requiredEntity = new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new("book", "books"), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Dictionary entityMap = new() + Runtime = configuration.Runtime + with { - { "Book", requiredEntity } - }; + Host = configuration.Runtime.Host + with + { Mode = hostModeType } + } + }; + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText( + CUSTOM_CONFIG, + configuration.ToJson()); - CreateCustomConfigFile(globalRestEnabled: globalRestEnabled, entityMap); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG}" + }; - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" - }; + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + HttpRequestMessage initialRequest = new(HttpMethod.Get, swaggerEndpoint); - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); + // Adding the following headers simulates an interactive browser request. + initialRequest.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); + initialRequest.Headers.Add("accept", BROWSER_ACCEPT_HEADER); - // Validate response + HttpResponseMessage response = await client.SendAsync(initialRequest); if (expectsError) { - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + // Redirect(HTTP 301) and follow up request to the returned path + // do not occur in a failure scenario. Only HTTP 400 (Bad Request) + // is expected. + Assert.AreEqual(expectedStatusCode, response.StatusCode); } else { - // Process response body - string responseBody = await response.Content.ReadAsStringAsync(); - Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); + // Swagger endpoint internally configured to reroute from /swagger to /swagger/index.html + Assert.AreEqual(HttpStatusCode.MovedPermanently, response.StatusCode); + + HttpRequestMessage followUpRequest = new(HttpMethod.Get, response.Headers.Location); + HttpResponseMessage followUpResponse = await client.SendAsync(followUpRequest); + Assert.AreEqual(expectedStatusCode, followUpResponse.StatusCode); - // Validate response body - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); + // Validate that Swagger requests OpenAPI document using REST path defined in runtime config. + string actualBody = await followUpResponse.Content.ReadAsStringAsync(); + Assert.AreEqual(true, actualBody.Contains(expectedOpenApiTargetContent)); } } + } - /// - /// Validates the behavior of the OpenApiDocumentor when the runtime config has entities with - /// REST endpoint enabled and disabled. - /// Enabled -> path should be created - /// Disabled -> path not created and is excluded from OpenApi document. - /// - [TestCategory(TestCategory.MSSQL)] - [TestMethod] - public async Task OpenApi_EntityLevelRestEndpoint() + /// + /// Validates the OpenAPI documentor behavior when enabling and disabling the global REST endpoint + /// for the DAB engine. + /// Global REST enabled: + /// - GET to /openapi returns the created OpenAPI document and succeeds with 200 OK. + /// Global REST disabled: + /// - GET to /openapi fails with 404 Not Found. + /// + [DataTestMethod] + [DataRow(true, false, DisplayName = "Global REST endpoint enabled - successful OpenAPI doc retrieval")] + [DataRow(false, true, DisplayName = "Global REST endpoint disabled - OpenAPI doc does not exist - HTTP404 NotFound.")] + [TestCategory(TestCategory.MSSQL)] + public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expectsError) + { + // At least one entity is required in the runtime config for the engine to start. + // Even though this entity is not under test, it must be supplied to the config + // file creation function. + Entity requiredEntity = new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new("book", "books"), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() { - // Create the entities under test. - Entity restEnabledEntity = new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - GraphQL: new("", "", false), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Entity restDisabledEntity = new( - Source: new("publishers", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false), - GraphQL: new("publisher", "publishers", true), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Dictionary entityMap = new() - { - { "Book", restEnabledEntity }, - { "Publisher", restDisabledEntity } - }; + { "Book", requiredEntity } + }; - CreateCustomConfigFile(globalRestEnabled: true, entityMap); + CreateCustomConfigFile(globalRestEnabled: globalRestEnabled, entityMap); - string[] args = new[] - { - $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" - }; + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" + }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Core.Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); - HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + // Setup and send GET request + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); - // Parse response metadata + // Validate response + if (expectsError) + { + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } + else + { + // Process response body string responseBody = await response.Content.ReadAsStringAsync(); Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); - // Validate response metadata + // Validate response body + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); - JsonElement pathsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS]; + } + } - // Validate that paths were created for the entity with REST enabled. - Assert.IsTrue(pathsElement.TryGetProperty("/Book", out _)); - Assert.IsTrue(pathsElement.TryGetProperty("/Book/id/{id}", out _)); + /// + /// Validates the behavior of the OpenApiDocumentor when the runtime config has entities with + /// REST endpoint enabled and disabled. + /// Enabled -> path should be created + /// Disabled -> path not created and is excluded from OpenApi document. + /// + [TestCategory(TestCategory.MSSQL)] + [TestMethod] + public async Task OpenApi_EntityLevelRestEndpoint() + { + // Create the entities under test. + Entity restEnabledEntity = new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + GraphQL: new("", "", false), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Entity restDisabledEntity = new( + Source: new("publishers", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false), + GraphQL: new("publisher", "publishers", true), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() + { + { "Book", restEnabledEntity }, + { "Publisher", restDisabledEntity } + }; - // Validate that paths were not created for the entity with REST disabled. - Assert.IsFalse(pathsElement.TryGetProperty("/Publisher", out _)); - Assert.IsFalse(pathsElement.TryGetProperty("/Publisher/id/{id}", out _)); + CreateCustomConfigFile(globalRestEnabled: true, entityMap); - JsonElement componentsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS]; - Assert.IsTrue(componentsElement.TryGetProperty(OpenApiDocumentorConstants.PROPERTY_SCHEMAS, out JsonElement componentSchemasElement)); - // Validate that components were created for the entity with REST enabled. - Assert.IsTrue(componentSchemasElement.TryGetProperty("Book_NoPK", out _)); - Assert.IsTrue(componentSchemasElement.TryGetProperty("Book", out _)); + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" + }; + + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + // Setup and send GET request + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Core.Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); + HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); + + // Parse response metadata + string responseBody = await response.Content.ReadAsStringAsync(); + Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); + + // Validate response metadata + ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); + JsonElement pathsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS]; + + // Validate that paths were created for the entity with REST enabled. + Assert.IsTrue(pathsElement.TryGetProperty("/Book", out _)); + Assert.IsTrue(pathsElement.TryGetProperty("/Book/id/{id}", out _)); + + // Validate that paths were not created for the entity with REST disabled. + Assert.IsFalse(pathsElement.TryGetProperty("/Publisher", out _)); + Assert.IsFalse(pathsElement.TryGetProperty("/Publisher/id/{id}", out _)); + + JsonElement componentsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS]; + Assert.IsTrue(componentsElement.TryGetProperty(OpenApiDocumentorConstants.PROPERTY_SCHEMAS, out JsonElement componentSchemasElement)); + // Validate that components were created for the entity with REST enabled. + Assert.IsTrue(componentSchemasElement.TryGetProperty("Book_NoPK", out _)); + Assert.IsTrue(componentSchemasElement.TryGetProperty("Book", out _)); + + // Validate that components were not created for the entity with REST disabled. + Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher_NoPK", out _)); + Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher", out _)); + } - // Validate that components were not created for the entity with REST disabled. - Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher_NoPK", out _)); - Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher", out _)); - } + /// + /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings + /// using the supplied entities. + /// + /// flag to enable or disabled REST globally. + /// Collection of entityName -> Entity object. + private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary entityMap) + { + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - /// - /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings - /// using the supplied entities. - /// - /// flag to enable or disabled REST globally. - /// Collection of entityName -> Entity object. - private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary entityMap) - { - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - - RuntimeConfig runtimeConfig = new( - Schema: string.Empty, - DataSource: dataSource, - Runtime: new( - Rest: new(Enabled: globalRestEnabled), - GraphQL: new(), - Host: new(null, null) - ), - Entities: new(entityMap)); - - File.WriteAllText( - path: CUSTOM_CONFIG_FILENAME, - contents: runtimeConfig.ToJson()); - } + RuntimeConfig runtimeConfig = new( + Schema: string.Empty, + DataSource: dataSource, + Runtime: new( + Rest: new(Enabled: globalRestEnabled), + GraphQL: new(), + Host: new(null, null) + ), + Entities: new(entityMap)); - /// - /// Validates that all the OpenAPI description document's top level properties exist. - /// A failure here indicates that there was an undetected failure creating the OpenAPI document. - /// - /// Represent a deserialized JSON result from retrieving the OpenAPI document - private static void ValidateOpenApiDocTopLevelPropertiesExist(Dictionary responseProperties) - { - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_OPENAPI)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_INFO)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_SERVERS)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS)); - } + File.WriteAllText( + path: CUSTOM_CONFIG_FILENAME, + contents: runtimeConfig.ToJson()); + } - /// - /// Validates that schema introspection requests fail when allow-introspection is false in the runtime configuration. - /// - /// - private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, HttpClient client, bool expectError) - { - string graphQLQueryName = "__schema"; - string graphQLQuery = @"{ + /// + /// Validates that all the OpenAPI description document's top level properties exist. + /// A failure here indicates that there was an undetected failure creating the OpenAPI document. + /// + /// Represent a deserialized JSON result from retrieving the OpenAPI document + private static void ValidateOpenApiDocTopLevelPropertiesExist(Dictionary responseProperties) + { + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_OPENAPI)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_INFO)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_SERVERS)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS)); + } + + /// + /// Validates that schema introspection requests fail when allow-introspection is false in the runtime configuration. + /// + /// + private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, HttpClient client, bool expectError) + { + string graphQLQueryName = "__schema"; + string graphQLQuery = @"{ __schema { types { name @@ -1506,239 +1506,239 @@ private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, } }"; - string expectedErrorMessageFragment = "Introspection is not allowed for the current request."; + string expectedErrorMessageFragment = "Introspection is not allowed for the current request."; - try - { - RuntimeConfigProvider configProvider = server.Services.GetRequiredService(); - - JsonElement actual = await GraphQLRequestExecutor.PostGraphQLRequestAsync( - client, - configProvider, - query: graphQLQuery, - queryName: graphQLQueryName, - variables: null, - clientRoleHeader: null - ); - - if (expectError) - { - SqlTestHelper.TestForErrorInGraphQLResponse( - response: actual.ToString(), - message: expectedErrorMessageFragment, - statusCode: ErrorCodes.Validation.IntrospectionNotAllowed - ); - } - } - catch (Exception ex) + try + { + RuntimeConfigProvider configProvider = server.Services.GetRequiredService(); + + JsonElement actual = await GraphQLRequestExecutor.PostGraphQLRequestAsync( + client, + configProvider, + query: graphQLQuery, + queryName: graphQLQueryName, + variables: null, + clientRoleHeader: null + ); + + if (expectError) { - // ExecuteGraphQLRequestAsync will raise an exception when no "data" key - // exists in the GraphQL JSON response. - Assert.Fail(message: "No schema metadata in GraphQL response." + ex.Message); + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessageFragment, + statusCode: ErrorCodes.Validation.IntrospectionNotAllowed + ); } } + catch (Exception ex) + { + // ExecuteGraphQLRequestAsync will raise an exception when no "data" key + // exists in the GraphQL JSON response. + Assert.Fail(message: "No schema metadata in GraphQL response." + ex.Message); + } + } - private static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false) + private static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false) + { + if (CONFIGURATION_ENDPOINT == endpoint) { - if (CONFIGURATION_ENDPOINT == endpoint) + ConfigurationPostParameters configParams = GetCosmosConfigurationParameters(); + if (config != null) { - ConfigurationPostParameters configParams = GetCosmosConfigurationParameters(); - if (config != null) - { - configParams = configParams with { Configuration = config }; - } - - if (useAccessToken) - { - configParams = configParams with - { - ConnectionString = "AccountEndpoint=https://localhost:8081/;", - AccessToken = GenerateMockJwtToken() - }; - } - - return JsonContent.Create(configParams); + configParams = configParams with { Configuration = config }; } - else if (CONFIGURATION_ENDPOINT_V2 == endpoint) - { - ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2(); - if (config != null) - { - configParams = configParams with { Configuration = config }; - } - - if (useAccessToken) - { - // With an invalid access token, when a new instance of CosmosClient is created with that token, it - // won't throw an exception. But when a graphql request is coming in, that's when it throws a 401 - // exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property - // from the token, if it's not valid, then we will throw an exception from our code before it - // initiating a client. Uses a valid fake JWT access token for testing purposes. - RuntimeConfig overrides = new(null, new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()), null, null); - - configParams = configParams with - { - ConfigurationOverrides = overrides.ToJson(), - AccessToken = GenerateMockJwtToken() - }; - } - return JsonContent.Create(configParams); - } - else + if (useAccessToken) { - throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}"); + configParams = configParams with + { + ConnectionString = "AccountEndpoint=https://localhost:8081/;", + AccessToken = GenerateMockJwtToken() + }; } - } - private static string GenerateMockJwtToken() + return JsonContent.Create(configParams); + } + else if (CONFIGURATION_ENDPOINT_V2 == endpoint) { - string mySecret = "PlaceholderPlaceholder"; - SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret)); + ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2(); + if (config != null) + { + configParams = configParams with { Configuration = config }; + } - JwtSecurityTokenHandler tokenHandler = new(); - SecurityTokenDescriptor tokenDescriptor = new() + if (useAccessToken) { - Subject = new ClaimsIdentity(new Claim[] { }), - Expires = DateTime.UtcNow.AddMinutes(5), - Issuer = "http://mysite.com", - Audience = "http://myaudience.com", - SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature) - }; + // With an invalid access token, when a new instance of CosmosClient is created with that token, it + // won't throw an exception. But when a graphql request is coming in, that's when it throws a 401 + // exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property + // from the token, if it's not valid, then we will throw an exception from our code before it + // initiating a client. Uses a valid fake JWT access token for testing purposes. + RuntimeConfig overrides = new(null, new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()), null, null); + + configParams = configParams with + { + ConfigurationOverrides = overrides.ToJson(), + AccessToken = GenerateMockJwtToken() + }; + } - SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); + return JsonContent.Create(configParams); } - - private static ConfigurationPostParameters GetCosmosConfigurationParameters() + else { - string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; - return new( - File.ReadAllText(cosmosFile), - File.ReadAllText("schema.gql"), - $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", - AccessToken: null); + throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}"); } + } - private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2() - { - string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; - RuntimeConfig overrides = new( - null, - new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()), - null, - null); - - return new( - File.ReadAllText(cosmosFile), - overrides.ToJson(), - File.ReadAllText("schema.gql"), - AccessToken: null); - } + private static string GenerateMockJwtToken() + { + string mySecret = "PlaceholderPlaceholder"; + SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret)); - /// - /// Helper used to create the post-startup configuration payload sent to configuration controller. - /// Adds entity used to hydrate authorization resolver post-startup and validate that hydration succeeds. - /// Additional pre-processing performed acquire database connection string from a local file. - /// - /// ConfigurationPostParameters object. - private static JsonContent GetPostStartupConfigParams(string environment, RuntimeConfig runtimeConfig, string configurationEndpoint) + JwtSecurityTokenHandler tokenHandler = new(); + SecurityTokenDescriptor tokenDescriptor = new() { - string connectionString = GetConnectionStringFromEnvironmentConfig(environment); + Subject = new ClaimsIdentity(new Claim[] { }), + Expires = DateTime.UtcNow.AddMinutes(5), + Issuer = "http://mysite.com", + Audience = "http://myaudience.com", + SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature) + }; + + SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } - string serializedConfiguration = runtimeConfig.ToJson(); + private static ConfigurationPostParameters GetCosmosConfigurationParameters() + { + string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; + return new( + File.ReadAllText(cosmosFile), + File.ReadAllText("schema.gql"), + $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", + AccessToken: null); + } - if (configurationEndpoint == CONFIGURATION_ENDPOINT) - { - ConfigurationPostParameters returnParams = new( - Configuration: serializedConfiguration, - Schema: null, - ConnectionString: connectionString, - AccessToken: null); - return JsonContent.Create(returnParams); - } - else if (configurationEndpoint == CONFIGURATION_ENDPOINT_V2) - { - RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()), null, null); + private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2() + { + string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; + RuntimeConfig overrides = new( + null, + new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()), + null, + null); + + return new( + File.ReadAllText(cosmosFile), + overrides.ToJson(), + File.ReadAllText("schema.gql"), + AccessToken: null); + } - ConfigurationPostParametersV2 returnParams = new( - Configuration: serializedConfiguration, - ConfigurationOverrides: overrides.ToJson(), - Schema: null, - AccessToken: null); + /// + /// Helper used to create the post-startup configuration payload sent to configuration controller. + /// Adds entity used to hydrate authorization resolver post-startup and validate that hydration succeeds. + /// Additional pre-processing performed acquire database connection string from a local file. + /// + /// ConfigurationPostParameters object. + private static JsonContent GetPostStartupConfigParams(string environment, RuntimeConfig runtimeConfig, string configurationEndpoint) + { + string connectionString = GetConnectionStringFromEnvironmentConfig(environment); - return JsonContent.Create(returnParams); - } - else - { - throw new InvalidOperationException("Invalid configurationEndpoint"); - } - } + string serializedConfiguration = runtimeConfig.ToJson(); - /// - /// Hydrates configuration after engine has started and triggers service instantiation - /// by executing HTTP requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// Post-startup configuration - /// ServiceUnavailable if service is not successfully hydrated with config - private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) + if (configurationEndpoint == CONFIGURATION_ENDPOINT) { - // Hydrate configuration post-startup - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - - return await GetRestResponsePostConfigHydration(httpClient); + ConfigurationPostParameters returnParams = new( + Configuration: serializedConfiguration, + Schema: null, + ConnectionString: connectionString, + AccessToken: null); + return JsonContent.Create(returnParams); } + else if (configurationEndpoint == CONFIGURATION_ENDPOINT_V2) + { + RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()), null, null); - /// - /// Executing REST requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// ServiceUnavailable if service is not successfully hydrated with config, - /// else the response code from the REST request - private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) + ConfigurationPostParametersV2 returnParams = new( + Configuration: serializedConfiguration, + ConfigurationOverrides: overrides.ToJson(), + Schema: null, + AccessToken: null); + + return JsonContent.Create(returnParams); + } + else { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; - HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) - { - // Spot test authorization resolver utilization to ensure configuration is used. - HttpResponseMessage postConfigHydrationResult = - await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); - responseCode = postConfigHydrationResult.StatusCode; + throw new InvalidOperationException("Invalid configurationEndpoint"); + } + } - if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) - { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); - continue; - } + /// + /// Hydrates configuration after engine has started and triggers service instantiation + /// by executing HTTP requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// Post-startup configuration + /// ServiceUnavailable if service is not successfully hydrated with config + private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) + { + // Hydrate configuration post-startup + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - break; + return await GetRestResponsePostConfigHydration(httpClient); + } + + /// + /// Executing REST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the REST request + private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) + { + // Retry request RETRY_COUNT times in 1 second increments to allow required services + // time to instantiate and hydrate permissions. + int retryCount = RETRY_COUNT; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount > 0) + { + // Spot test authorization resolver utilization to ensure configuration is used. + HttpResponseMessage postConfigHydrationResult = + await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); + responseCode = postConfigHydrationResult.StatusCode; + + if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + retryCount--; + Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + continue; } - return responseCode; + break; } - /// - /// Executing GraphQL POST requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// ServiceUnavailable if service is not successfully hydrated with config, - /// else the response code from the GRAPHQL request - private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) + return responseCode; + } + + /// + /// Executing GraphQL POST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the GRAPHQL request + private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) + { + // Retry request RETRY_COUNT times in 1 second increments to allow required services + // time to instantiate and hydrate permissions. + int retryCount = RETRY_COUNT; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount > 0) { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; - HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) - { - string query = @"{ + string query = @"{ book_by_pk(id: 1) { id, title, @@ -1746,130 +1746,129 @@ private static async Task GetGraphQLResponsePostConfigHydration( } }"; - object payload = new { query }; - - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + object payload = new { query }; - HttpResponseMessage graphQLResponse = await httpClient.SendAsync(graphQLRequest); - responseCode = graphQLResponse.StatusCode; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - if (responseCode == HttpStatusCode.ServiceUnavailable) - { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); - continue; - } + HttpResponseMessage graphQLResponse = await httpClient.SendAsync(graphQLRequest); + responseCode = graphQLResponse.StatusCode; - break; + if (responseCode == HttpStatusCode.ServiceUnavailable) + { + retryCount--; + Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + continue; } - return responseCode; + break; } - /// - /// Instantiate minimal runtime config with custom global settings. - /// - /// DataSource to pull connection string required for engine start. - /// - public static RuntimeConfig InitMinimalRuntimeConfig( - DataSource dataSource, - GraphQLRuntimeOptions graphqlOptions, - RestRuntimeOptions restOptions, - Entity entity = null, - string entityName = null) - { - entity ??= new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: null, - GraphQL: new(Singular: "book", Plural: "books"), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null - ); - - entityName ??= "Book"; - - Dictionary entityMap = new() - { - { entityName, entity } - }; + return responseCode; + } - return new( - Schema: "IntegrationTestMinimalSchema", - DataSource: dataSource, - Runtime: new(restOptions, graphqlOptions, new(null, null)), - Entities: new(entityMap) + /// + /// Instantiate minimal runtime config with custom global settings. + /// + /// DataSource to pull connection string required for engine start. + /// + public static RuntimeConfig InitMinimalRuntimeConfig( + DataSource dataSource, + GraphQLRuntimeOptions graphqlOptions, + RestRuntimeOptions restOptions, + Entity entity = null, + string entityName = null) + { + entity ??= new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: null, + GraphQL: new(Singular: "book", Plural: "books"), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null ); - } - /// - /// Gets PermissionSetting object allowed to perform all actions. - /// - /// Name of role to assign to permission - /// PermissionSetting - public static EntityPermission GetMinimalPermissionConfig(string roleName) + entityName ??= "Book"; + + Dictionary entityMap = new() { - EntityAction actionForRole = new( - Action: EntityActionOperation.All, - Fields: null, - Policy: new() - ); + { entityName, entity } + }; + + return new( + Schema: "IntegrationTestMinimalSchema", + DataSource: dataSource, + Runtime: new(restOptions, graphqlOptions, new(null, null)), + Entities: new(entityMap) + ); + } - return new EntityPermission( - Role: roleName, - Actions: new[] { actionForRole } - ); - } + /// + /// Gets PermissionSetting object allowed to perform all actions. + /// + /// Name of role to assign to permission + /// PermissionSetting + public static EntityPermission GetMinimalPermissionConfig(string roleName) + { + EntityAction actionForRole = new( + Action: EntityActionOperation.All, + Fields: null, + Policy: new() + ); + + return new EntityPermission( + Role: roleName, + Actions: new[] { actionForRole } + ); + } - /// - /// Reads configuration file for defined environment to acquire the connection string. - /// CI/CD Pipelines and local environments may not have connection string set as environment variable. - /// - /// Environment such as TestCategory.MSSQL - /// Connection string - public static string GetConnectionStringFromEnvironmentConfig(string environment) - { - FileSystem fileSystem = new(); - string sqlFile = new RuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); - string configPayload = File.ReadAllText(sqlFile); + /// + /// Reads configuration file for defined environment to acquire the connection string. + /// CI/CD Pipelines and local environments may not have connection string set as environment variable. + /// + /// Environment such as TestCategory.MSSQL + /// Connection string + public static string GetConnectionStringFromEnvironmentConfig(string environment) + { + FileSystem fileSystem = new(); + string sqlFile = new RuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); + string configPayload = File.ReadAllText(sqlFile); - RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig); + RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig); - return runtimeConfig.DataSource.ConnectionString; - } + return runtimeConfig.DataSource.ConnectionString; + } - private static void ValidateCosmosDbSetup(TestServer server) - { - object metadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(metadataProvider, typeof(CosmosSqlMetadataProvider)); + private static void ValidateCosmosDbSetup(TestServer server) + { + object metadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(metadataProvider, typeof(CosmosSqlMetadataProvider)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(CosmosQueryEngine)); + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(CosmosQueryEngine)); - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(CosmosMutationEngine)); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(CosmosMutationEngine)); - CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.IsNotNull(cosmosClientProvider); - Assert.IsNotNull(cosmosClientProvider.Client); - } + CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.IsNotNull(cosmosClientProvider); + Assert.IsNotNull(cosmosClientProvider.Client); + } - private bool HandleException(Exception e) where T : Exception + private bool HandleException(Exception e) where T : Exception + { + if (e is AggregateException aggregateException) { - if (e is AggregateException aggregateException) - { - aggregateException.Handle(HandleException); - return true; - } - else if (e is T) - { - return true; - } - - return false; + aggregateException.Handle(HandleException); + return true; + } + else if (e is T) + { + return true; } + + return false; } } diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index cf0748c74a..96c46005ee 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -3,54 +3,64 @@ using System; using System.Collections.Generic; -using System.Data; using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Azure.DataApiBuilder.Service.Tests.OpenApiDocumentor +namespace Azure.DataApiBuilder.Service.Tests.OpenApiDocumentor; + +/// +/// Validates TypeHelper converters return expected results. +/// +[TestClass] +public class CLRtoJsonValueTypeUnitTests { + private const string ERROR_PREFIX = "The SqlDbType "; + private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; + private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; + private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; + + /// + /// Validates that all DAB supported CLR types (system types) map to a defined JSON value type. + /// A DAB supported CLR type is a CLR type mapped from a database value type. + /// + [TestMethod] + [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] + public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bool isSupportedSqlDataType) + { + try + { + Type resolvedType = TypeHelper.GetSystemTypeFromSqlDbType(sqlDataTypeLiteral); + Assert.AreEqual(true, isSupportedSqlDataType, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + + JsonDataType resolvedJsonType = TypeHelper.GetJsonDataTypeFromSystemType(resolvedType); + Assert.AreEqual(isSupportedSqlDataType, resolvedJsonType != JsonDataType.Undefined, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + JSONDATATYPE_RESOLUTION_ERROR); + } + catch (DataApiBuilderException) + { + Assert.AreEqual(false, isSupportedSqlDataType, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + } + } + /// - /// Validates TypeHelper converters return expected results. + /// Generates test cases for use in DynamicData method + /// SupportedSystemTypesMapToJsonValueType + /// Test cases will be named like: SupportedSystemTypesMapToJsonValueType (date,True) + /// where 'date' is pair.key and 'True' is pair.Value from the source dictionary. /// - [TestClass] - public class CLRtoJsonValueTypeUnitTests + /// Enumerator over object arrays with test case input data. + /// + private static IEnumerable GetTestData() { - private const string ERROR_PREFIX = "The SqlDbType "; - private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; - private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; - private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; - - /// - /// Validates that all DAB supported CLR types (system types) map to a defined JSON value type. - /// A DAB supported CLR type is a CLR type mapped from a database value type. - /// - [TestMethod] - public void SupportedSystemTypesMapToJsonValueType() + List testCases = new(); + + foreach (KeyValuePair pair in SqlTypeConstants.SupportedSqlDbTypes) { - foreach (KeyValuePair sqlDataType in SqlTypeConstants.SupportedSqlDbTypes) - { - string sqlDataTypeLiteral = sqlDataType.Key; - bool isSupportedSqlDataType = sqlDataType.Value; - - try - { - Type resolvedType = TypeHelper.GetSystemTypeFromSqlDbType(sqlDataTypeLiteral); - Assert.AreEqual(true, isSupportedSqlDataType, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); - - JsonDataType resolvedJsonType = TypeHelper.GetJsonDataTypeFromSystemType(resolvedType); - Assert.AreEqual(isSupportedSqlDataType, resolvedJsonType != JsonDataType.Undefined, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + JSONDATATYPE_RESOLUTION_ERROR); - - DbType? resolvedDbType = TypeHelper.GetDbTypeFromSystemType(resolvedType); - Assert.AreEqual(isSupportedSqlDataType, resolvedDbType is not null, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + DBTYPE_RESOLUTION_ERROR); - } - catch (DataApiBuilderException) - { - Assert.AreEqual(false, isSupportedSqlDataType, ERROR_PREFIX + $" {{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); - } - } + testCases.Add(new object[] { pair.Key, pair.Value }); } + + return testCases; } } From afda68df335c24f55a9d628ded4dee47c0133cd8 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 13:51:11 -0700 Subject: [PATCH 05/11] add additional unit test demonstrating proper resolution of nullable value types to their underlying types, also adds comments to explain in tests and in engine code (TypeHelper) --- src/Core/Services/TypeHelper.cs | 20 +++------ .../CLRtoJsonValueTypeUnitTests.cs | 42 +++++++++++++++++-- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 4030d0b919..92eb836165 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -16,7 +16,6 @@ public static class TypeHelper { /// /// Maps .NET Framework types to DbType enum - /// Unnecessary to add nullable types because /// private static Dictionary _systemTypeToDbTypeMap = new() { @@ -56,6 +55,9 @@ public static class TypeHelper /// /// Maps .NET Framework type (System/CLR type) to JsonDataType. + /// Unnecessary to add nullable types because GetJsonDataTypeFromSystemType() + /// (the helper used to access key/values in this dictionary) + /// resolves the underlying type when a nullable type is used for lookup. /// private static Dictionary _systemTypeToJsonDataTypeMap = new() { @@ -76,20 +78,6 @@ public static class TypeHelper [typeof(Guid)] = JsonDataType.String, [typeof(byte[])] = JsonDataType.String, [typeof(TimeSpan)] = JsonDataType.String, - [typeof(byte?)] = JsonDataType.String, - [typeof(sbyte?)] = JsonDataType.String, - [typeof(short?)] = JsonDataType.Number, - [typeof(ushort?)] = JsonDataType.Number, - [typeof(int?)] = JsonDataType.Number, - [typeof(uint?)] = JsonDataType.Number, - [typeof(long?)] = JsonDataType.Number, - [typeof(ulong?)] = JsonDataType.Number, - [typeof(float?)] = JsonDataType.Number, - [typeof(double?)] = JsonDataType.Number, - [typeof(decimal?)] = JsonDataType.Number, - [typeof(bool?)] = JsonDataType.Boolean, - [typeof(char?)] = JsonDataType.String, - [typeof(Guid?)] = JsonDataType.String, [typeof(object)] = JsonDataType.Object, [typeof(DateTime)] = JsonDataType.String, [typeof(DateTimeOffset)] = JsonDataType.String @@ -142,6 +130,8 @@ public static JsonDataType GetJsonDataTypeFromSystemType(Type type) { // Get the underlying type argument if the 'type' argument is a nullable type. Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); + + // Will not be null when the input argument 'type' is a closed generic nullable type. if (nullableUnderlyingType is not null) { type = nullableUnderlyingType; diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 96c46005ee..e04674e3ad 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - +#nullable enable using System; using System.Collections.Generic; using Azure.DataApiBuilder.Core.Services; @@ -27,7 +27,7 @@ public class CLRtoJsonValueTypeUnitTests /// A DAB supported CLR type is a CLR type mapped from a database value type. /// [TestMethod] - [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] + [DynamicData(nameof(GetTestData_SupportedSystemTypesMapToJsonValueType), DynamicDataSourceType.Method)] public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bool isSupportedSqlDataType) { try @@ -52,7 +52,7 @@ public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bo /// /// Enumerator over object arrays with test case input data. /// - private static IEnumerable GetTestData() + private static IEnumerable GetTestData_SupportedSystemTypesMapToJsonValueType() { List testCases = new(); @@ -63,4 +63,40 @@ private static IEnumerable GetTestData() return testCases; } + + /// + /// Validates the behavior of TypeHelper.GetJsonDataTypeFromSystemType(Type type) by + /// ensuring that a nullable value type like int? is resolved to its underlying type int. + /// Consequently, the lookup in the _systemTypeToJsonDataTypeMap dictionary succeeds without + /// requiring nullable value type be defined as keys. + /// Nullable value types are represented in runtime as Nullable. Whereas + /// nullable reference types do no have a standalone runtime representation. + /// See csharplang discussion on why typeof(string?) (nullable reference type) is not valid, + /// and that the type encountered during runtime for string? would be string. + /// + /// + /// + [DataRow(typeof(int?))] + [DataRow(typeof(byte?))] + [DataRow(typeof(sbyte?))] + [DataRow(typeof(short?))] + [DataRow(typeof(ushort?))] + [DataRow(typeof(int?))] + [DataRow(typeof(uint?))] + [DataRow(typeof(long?))] + [DataRow(typeof(ulong?))] + [DataRow(typeof(float?))] + [DataRow(typeof(double?))] + [DataRow(typeof(decimal?))] + [DataRow(typeof(bool?))] + [DataRow(typeof(char?))] + [DataRow(typeof(Guid?))] + [DataRow(typeof(TimeSpan?))] + [DataRow(typeof(DateTime?))] + [DataRow(typeof(DateTimeOffset?))] + [DataTestMethod] + public void ResolveUnderlyingTypeForNullableValueType(Type nullableType) + { + Assert.AreNotEqual(notExpected: JsonDataType.Undefined, actual: TypeHelper.GetJsonDataTypeFromSystemType(nullableType)); + } } From 29a8b8e7e42632894a0a6c06c2f00b46d6725aaa Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 13:53:05 -0700 Subject: [PATCH 06/11] Address review feedback: revert filescoped namespace change to highlight diffs that this PR actually contributes, as it doesn't change the whole file as github currently suggests. Updates TypeHelper dictionary to be defined by providing an enumerable of key/value pairs instead of indexer assignment. --- src/Core/Services/TypeHelper.cs | 323 ++++++++++++++++---------------- 1 file changed, 162 insertions(+), 161 deletions(-) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index 92eb836165..b38ddf7c6f 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -6,188 +6,189 @@ using Azure.DataApiBuilder.Core.Services.OpenAPI; using Azure.DataApiBuilder.Service.Exceptions; -namespace Azure.DataApiBuilder.Core.Services; - -/// -/// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types. -/// -/// -public static class TypeHelper +namespace Azure.DataApiBuilder.Core.Services { /// - /// Maps .NET Framework types to DbType enum + /// Type mapping helpers to convert between SQL Server types, .NET Framework types, and Json value types. /// - private static Dictionary _systemTypeToDbTypeMap = new() + /// + public static class TypeHelper { - [typeof(byte)] = DbType.Byte, - [typeof(sbyte)] = DbType.SByte, - [typeof(short)] = DbType.Int16, - [typeof(ushort)] = DbType.UInt16, - [typeof(int)] = DbType.Int32, - [typeof(uint)] = DbType.UInt32, - [typeof(long)] = DbType.Int64, - [typeof(ulong)] = DbType.UInt64, - [typeof(float)] = DbType.Single, - [typeof(double)] = DbType.Double, - [typeof(decimal)] = DbType.Decimal, - [typeof(bool)] = DbType.Boolean, - [typeof(string)] = DbType.String, - [typeof(char)] = DbType.StringFixedLength, - [typeof(Guid)] = DbType.Guid, - [typeof(byte[])] = DbType.Binary, - [typeof(DateTimeOffset)] = DbType.DateTimeOffset, - [typeof(byte?)] = DbType.Byte, - [typeof(sbyte?)] = DbType.SByte, - [typeof(short?)] = DbType.Int16, - [typeof(ushort?)] = DbType.UInt16, - [typeof(int?)] = DbType.Int32, - [typeof(uint?)] = DbType.UInt32, - [typeof(long?)] = DbType.Int64, - [typeof(ulong?)] = DbType.UInt64, - [typeof(float?)] = DbType.Single, - [typeof(double?)] = DbType.Double, - [typeof(decimal?)] = DbType.Decimal, - [typeof(bool?)] = DbType.Boolean, - [typeof(char?)] = DbType.StringFixedLength, - [typeof(Guid?)] = DbType.Guid, - [typeof(object)] = DbType.Object - }; + /// + /// Maps .NET Framework types to DbType enum + /// + private static Dictionary _systemTypeToDbTypeMap = new() + { + [typeof(byte)] = DbType.Byte, + [typeof(sbyte)] = DbType.SByte, + [typeof(short)] = DbType.Int16, + [typeof(ushort)] = DbType.UInt16, + [typeof(int)] = DbType.Int32, + [typeof(uint)] = DbType.UInt32, + [typeof(long)] = DbType.Int64, + [typeof(ulong)] = DbType.UInt64, + [typeof(float)] = DbType.Single, + [typeof(double)] = DbType.Double, + [typeof(decimal)] = DbType.Decimal, + [typeof(bool)] = DbType.Boolean, + [typeof(string)] = DbType.String, + [typeof(char)] = DbType.StringFixedLength, + [typeof(Guid)] = DbType.Guid, + [typeof(byte[])] = DbType.Binary, + [typeof(DateTimeOffset)] = DbType.DateTimeOffset, + [typeof(byte?)] = DbType.Byte, + [typeof(sbyte?)] = DbType.SByte, + [typeof(short?)] = DbType.Int16, + [typeof(ushort?)] = DbType.UInt16, + [typeof(int?)] = DbType.Int32, + [typeof(uint?)] = DbType.UInt32, + [typeof(long?)] = DbType.Int64, + [typeof(ulong?)] = DbType.UInt64, + [typeof(float?)] = DbType.Single, + [typeof(double?)] = DbType.Double, + [typeof(decimal?)] = DbType.Decimal, + [typeof(bool?)] = DbType.Boolean, + [typeof(char?)] = DbType.StringFixedLength, + [typeof(Guid?)] = DbType.Guid, + [typeof(object)] = DbType.Object + }; - /// - /// Maps .NET Framework type (System/CLR type) to JsonDataType. + /// + /// Maps .NET Framework type (System/CLR type) to JsonDataType. /// Unnecessary to add nullable types because GetJsonDataTypeFromSystemType() /// (the helper used to access key/values in this dictionary) /// resolves the underlying type when a nullable type is used for lookup. - /// - private static Dictionary _systemTypeToJsonDataTypeMap = new() - { - [typeof(byte)] = JsonDataType.String, - [typeof(sbyte)] = JsonDataType.String, - [typeof(short)] = JsonDataType.Number, - [typeof(ushort)] = JsonDataType.Number, - [typeof(int)] = JsonDataType.Number, - [typeof(uint)] = JsonDataType.Number, - [typeof(long)] = JsonDataType.Number, - [typeof(ulong)] = JsonDataType.Number, - [typeof(float)] = JsonDataType.Number, - [typeof(double)] = JsonDataType.Number, - [typeof(decimal)] = JsonDataType.Number, - [typeof(bool)] = JsonDataType.Boolean, - [typeof(string)] = JsonDataType.String, - [typeof(char)] = JsonDataType.String, - [typeof(Guid)] = JsonDataType.String, - [typeof(byte[])] = JsonDataType.String, - [typeof(TimeSpan)] = JsonDataType.String, - [typeof(object)] = JsonDataType.Object, - [typeof(DateTime)] = JsonDataType.String, - [typeof(DateTimeOffset)] = JsonDataType.String - }; + /// + private static Dictionary _systemTypeToJsonDataTypeMap = new() + { + [typeof(byte)] = JsonDataType.String, + [typeof(sbyte)] = JsonDataType.String, + [typeof(short)] = JsonDataType.Number, + [typeof(ushort)] = JsonDataType.Number, + [typeof(int)] = JsonDataType.Number, + [typeof(uint)] = JsonDataType.Number, + [typeof(long)] = JsonDataType.Number, + [typeof(ulong)] = JsonDataType.Number, + [typeof(float)] = JsonDataType.Number, + [typeof(double)] = JsonDataType.Number, + [typeof(decimal)] = JsonDataType.Number, + [typeof(bool)] = JsonDataType.Boolean, + [typeof(string)] = JsonDataType.String, + [typeof(char)] = JsonDataType.String, + [typeof(Guid)] = JsonDataType.String, + [typeof(byte[])] = JsonDataType.String, + [typeof(TimeSpan)] = JsonDataType.String, + [typeof(object)] = JsonDataType.Object, + [typeof(DateTime)] = JsonDataType.String, + [typeof(DateTimeOffset)] = JsonDataType.String + }; - /// - /// Maps SqlDbType enum to .NET Framework type (System type). - /// - private static readonly Dictionary _sqlDbTypeToType = new() - { - { SqlDbType.BigInt,typeof(long) }, - { SqlDbType.Binary, typeof(byte) }, - { SqlDbType.Bit, typeof(bool)}, - { SqlDbType.Char, typeof(string) }, - { SqlDbType.Date, typeof(DateTime) }, - { SqlDbType.DateTime, typeof(DateTime)}, - { SqlDbType.DateTime2, typeof(DateTime)}, - { SqlDbType.DateTimeOffset, typeof(DateTimeOffset)}, - { SqlDbType.Decimal, typeof(decimal)}, - { SqlDbType.Float, typeof(double)}, - { SqlDbType.Image, typeof(byte[])}, - { SqlDbType.Int, typeof(int)}, - { SqlDbType.Money, typeof(decimal)}, - { SqlDbType.NChar, typeof(char)}, - { SqlDbType.NText, typeof(string)}, - { SqlDbType.NVarChar,typeof(string) }, - { SqlDbType.Real, typeof(float)}, - { SqlDbType.SmallDateTime, typeof(DateTime) }, - { SqlDbType.SmallInt, typeof(short) }, - { SqlDbType.SmallMoney, typeof(decimal) }, - { SqlDbType.Text, typeof(string)}, - { SqlDbType.Time, typeof(TimeSpan)}, - { SqlDbType.Timestamp, typeof(byte[])}, - { SqlDbType.TinyInt, typeof(byte) }, - { SqlDbType.UniqueIdentifier, typeof(Guid) }, - { SqlDbType.VarBinary, typeof(byte[]) }, - { SqlDbType.VarChar, typeof(string) } - }; + /// + /// Maps SqlDbType enum to .NET Framework type (System type). + /// + private static readonly Dictionary _sqlDbTypeToType = new() + { + [SqlDbType.BigInt] = typeof(long), + [SqlDbType.Binary] = typeof(byte), + [SqlDbType.Bit] = typeof(bool), + [SqlDbType.Char] = typeof(string), + [SqlDbType.Date] = typeof(DateTime), + [SqlDbType.DateTime] = typeof(DateTime), + [SqlDbType.DateTime2] = typeof(DateTime), + [SqlDbType.DateTimeOffset] = typeof(DateTimeOffset), + [SqlDbType.Decimal] = typeof(decimal), + [SqlDbType.Float] = typeof(double), + [SqlDbType.Image] = typeof(byte[]), + [SqlDbType.Int] = typeof(int), + [SqlDbType.Money] = typeof(decimal), + [SqlDbType.NChar] = typeof(char), + [SqlDbType.NText] = typeof(string), + [SqlDbType.NVarChar] = typeof(string), + [SqlDbType.Real] = typeof(float), + [SqlDbType.SmallDateTime] = typeof(DateTime), + [SqlDbType.SmallInt] = typeof(short), + [SqlDbType.SmallMoney] = typeof(decimal), + [SqlDbType.Text] = typeof(string), + [SqlDbType.Time] = typeof(TimeSpan), + [SqlDbType.Timestamp] = typeof(byte[]), + [SqlDbType.TinyInt] = typeof(byte), + [SqlDbType.UniqueIdentifier] = typeof(Guid), + [SqlDbType.VarBinary] = typeof(byte[]), + [SqlDbType.VarChar] = typeof(string) + }; - /// - /// Converts the .NET Framework (System/CLR) type to JsonDataType. - /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported - /// by the JSON Schema Specification Wright Draft 00. - /// The value returned is formatted for use in the OpenAPI spec "type" property. - /// - /// CLR type - /// - /// Formatted JSON type name in lower case: e.g. number, string, boolean, etc. - public static JsonDataType GetJsonDataTypeFromSystemType(Type type) - { - // Get the underlying type argument if the 'type' argument is a nullable type. - Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); + /// + /// Converts the .NET Framework (System/CLR) type to JsonDataType. + /// Primitive data types in the OpenAPI standard (OAS) are based on the types supported + /// by the JSON Schema Specification Wright Draft 00. + /// The value returned is formatted for use in the OpenAPI spec "type" property. + /// + /// CLR type + /// + /// Formatted JSON type name in lower case: e.g. number, string, boolean, etc. + public static JsonDataType GetJsonDataTypeFromSystemType(Type type) + { + // Get the underlying type argument if the 'type' argument is a nullable type. + Type? nullableUnderlyingType = Nullable.GetUnderlyingType(type); // Will not be null when the input argument 'type' is a closed generic nullable type. - if (nullableUnderlyingType is not null) - { - type = nullableUnderlyingType; - } + if (nullableUnderlyingType is not null) + { + type = nullableUnderlyingType; + } - if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName)) - { - openApiJsonTypeName = JsonDataType.Undefined; - } + if (!_systemTypeToJsonDataTypeMap.TryGetValue(type, out JsonDataType openApiJsonTypeName)) + { + openApiJsonTypeName = JsonDataType.Undefined; + } - return openApiJsonTypeName; - } + return openApiJsonTypeName; + } - /// - /// Returns the DbType for given system type. - /// - /// The system type for which the DbType is to be determined. - /// DbType for the given system type. Null when no mapping exists. - public static DbType? GetDbTypeFromSystemType(Type systemType) - { - if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) + /// + /// Returns the DbType for given system type. + /// + /// The system type for which the DbType is to be determined. + /// DbType for the given system type. Null when no mapping exists. + public static DbType? GetDbTypeFromSystemType(Type systemType) { - return null; - } + if (!_systemTypeToDbTypeMap.TryGetValue(systemType, out DbType dbType)) + { + return null; + } - return dbType; - } + return dbType; + } - /// - /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented - /// by the SQL Server data type mappings article. - /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. - /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. - /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however - /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. - /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', - /// however SqlDbType.Binary now exists. - /// - /// String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns - /// schema collections. - /// - /// - /// Failed type conversion." - public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName) - { - if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType)) + /// + /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented + /// by the SQL Server data type mappings article. + /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. + /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. + /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however + /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. + /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', + /// however SqlDbType.Binary now exists. + /// + /// String value sourced from the DATA_TYPE column in the Procedure Parameters or Columns + /// schema collections. + /// + /// + /// Failed type conversion." + public static Type GetSystemTypeFromSqlDbType(string sqlDbTypeName) { - if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value)) + if (Enum.TryParse(sqlDbTypeName, ignoreCase: true, out SqlDbType sqlDbType)) { - return value; + if (_sqlDbTypeToType.TryGetValue(sqlDbType, out Type? value)) + { + return value; + } } - } - throw new DataApiBuilderException( - message: $"Tried to convert unsupported data type: {sqlDbTypeName}", - statusCode: HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + throw new DataApiBuilderException( + message: $"Tried to convert unsupported data type: {sqlDbTypeName}", + statusCode: HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } } } From 9e914ab71352cbd4b2d380f0bfba35fe4ed30615 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 13:59:20 -0700 Subject: [PATCH 07/11] Update explanation that SQL Server data types also apply to Azure SQL DB --- src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs index be6cae38e8..15f080a783 100644 --- a/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs +++ b/src/Core/Services/MetadataProviders/MsSqlMetadataProvider.cs @@ -32,7 +32,8 @@ public override string GetDefaultSchemaName() } /// - /// Takes a string version of an SQL Server data type and returns its .NET common language runtime (CLR) counterpart + /// Takes a string version of an SQL Server data type (also applies to Azure SQL DB) + /// and returns its .NET common language runtime (CLR) counterpart /// As per https://docs.microsoft.com/dotnet/framework/data/adonet/sql-server-data-type-mappings /// public override Type SqlToCLRType(string sqlType) From 19186d9838f5d1819517d0b0e207582dd003b821 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 14:30:09 -0700 Subject: [PATCH 08/11] clearer comments. --- src/Core/Services/TypeHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index b38ddf7c6f..e4ab99df03 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -161,12 +161,12 @@ public static JsonDataType GetJsonDataTypeFromSystemType(Type type) } /// - /// Converts the string representation of a SQL Server data type to the corrsponding .NET Framework/CLR type as documented - /// by the SQL Server data type mappings article. + /// Converts the string representation of a SQL Server data type that can be parsed into SqlDbType enum + /// to the corrsponding .NET Framework/CLR type as documented by the SQL Server data type mappings article. /// The SQL Server database engine type and SqlDbType enum map 1:1 when character casing is ignored. /// e.g. SQL DB type 'bigint' maps to SqlDbType enum 'BigInt' in a case-insensitive match. /// There are some mappings in the SQL Server data type mappings table which do not map after ignoring casing, however - /// those mappings are outdated and don't accommodate newly added SqlDbType enum values added. + /// those mappings are outdated and don't accommodate newly added SqlDbType enum values. /// e.g. The documentation table shows SQL server type 'binary' maps to SqlDbType enum 'VarBinary', /// however SqlDbType.Binary now exists. /// From 9e3c9b618000d616b8d04a84cb8082291930d7b6 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 16:14:02 -0700 Subject: [PATCH 09/11] Updated failure messages and comments for CLRtoJsonValueTypeUnitTests; --- .../CLRtoJsonValueTypeUnitTests.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index e04674e3ad..4b23b98117 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -17,15 +17,22 @@ namespace Azure.DataApiBuilder.Service.Tests.OpenApiDocumentor; [TestClass] public class CLRtoJsonValueTypeUnitTests { + private const string STRING_PARSE_ERROR_PREFIX = "The input string value "; private const string ERROR_PREFIX = "The SqlDbType "; private const string SQLDBTYPE_RESOLUTION_ERROR = "failed to resolve to SqlDbType."; + private const string SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR = "should have resolved to a SqlDbType."; private const string JSONDATATYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated JsonDataType."; private const string DBTYPE_RESOLUTION_ERROR = "(when supported) should map to a system type and associated DbType."; /// - /// Validates that all DAB supported CLR types (system types) map to a defined JSON value type. + /// Validates that: + /// 1. String representations of SqlDbType provided by SQL Server/Azure SQL DB resolve to a SqlDbType enum + /// and CLR/system type. + /// 2. The resolved CLR/system types map to a defined JsonDataType. /// A DAB supported CLR type is a CLR type mapped from a database value type. /// + /// Raw string provided by database e.g. 'bigint' + /// Whether DAB supports the resolved SqlDbType value. [TestMethod] [DynamicData(nameof(GetTestData_SupportedSystemTypesMapToJsonValueType), DynamicDataSourceType.Method)] public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bool isSupportedSqlDataType) @@ -33,14 +40,14 @@ public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bo try { Type resolvedType = TypeHelper.GetSystemTypeFromSqlDbType(sqlDataTypeLiteral); - Assert.AreEqual(true, isSupportedSqlDataType, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + Assert.AreEqual(true, isSupportedSqlDataType, STRING_PARSE_ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); JsonDataType resolvedJsonType = TypeHelper.GetJsonDataTypeFromSystemType(resolvedType); Assert.AreEqual(isSupportedSqlDataType, resolvedJsonType != JsonDataType.Undefined, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + JSONDATATYPE_RESOLUTION_ERROR); } catch (DataApiBuilderException) { - Assert.AreEqual(false, isSupportedSqlDataType, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_RESOLUTION_ERROR); + Assert.AreEqual(false, isSupportedSqlDataType, ERROR_PREFIX + $"{{{sqlDataTypeLiteral}}} " + SQLDBTYPE_UNEXPECTED_RESOLUTION_ERROR); } } From e816f4f87fd7802ebb62214a4f6e885ed33ee41f Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Fri, 7 Jul 2023 16:14:47 -0700 Subject: [PATCH 10/11] removed [typeof(DateTimeOffset)] = DbType.DateTimeOffset, from _systemTypeToDbTypeMap to avoid any possible regression. --- src/Core/Services/TypeHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Services/TypeHelper.cs b/src/Core/Services/TypeHelper.cs index e4ab99df03..7c41909748 100644 --- a/src/Core/Services/TypeHelper.cs +++ b/src/Core/Services/TypeHelper.cs @@ -35,7 +35,6 @@ public static class TypeHelper [typeof(char)] = DbType.StringFixedLength, [typeof(Guid)] = DbType.Guid, [typeof(byte[])] = DbType.Binary, - [typeof(DateTimeOffset)] = DbType.DateTimeOffset, [typeof(byte?)] = DbType.Byte, [typeof(sbyte?)] = DbType.SByte, [typeof(short?)] = DbType.Int16, From 53cd4ca61e1cd3bb3b85411f19c3e909bf7ce757 Mon Sep 17 00:00:00 2001 From: Sean Leonard Date: Mon, 10 Jul 2023 12:11:42 -0700 Subject: [PATCH 11/11] update indentation on sqltypeconstants and add additional test to cover unexpected sqldbtyp. Also updated configurationtests to not have single line namespace to accurately show changes. --- src/Core/Models/SqlTypeConstants.cs | 62 +- .../Configuration/ConfigurationTests.cs | 2971 +++++++++-------- .../CLRtoJsonValueTypeUnitTests.cs | 1 + 3 files changed, 1518 insertions(+), 1516 deletions(-) diff --git a/src/Core/Models/SqlTypeConstants.cs b/src/Core/Models/SqlTypeConstants.cs index a435386715..66c4952665 100644 --- a/src/Core/Models/SqlTypeConstants.cs +++ b/src/Core/Models/SqlTypeConstants.cs @@ -18,35 +18,35 @@ public static class SqlTypeConstants /// public static readonly Dictionary SupportedSqlDbTypes = new() { - { "bigint", true }, // SqlDbType.BigInt - { "binary", true }, // SqlDbType.Binary - { "bit", true }, // SqlDbType.Bit - { "char", true }, // SqlDbType.Char - { "datetime", true }, // SqlDbType.DateTime - { "decimal", true }, // SqlDbType.Decimal - { "float", true }, // SqlDbType.Float - { "image", true }, // SqlDbType.Image - { "int", true }, // SqlDbType.Int - { "money", true }, // SqlDbType.Money - { "nchar", true }, // SqlDbType.NChar - { "ntext", true }, // SqlDbType.NText - { "nvarchar", true }, // SqlDbType.NVarChar - { "real", true }, // SqlDbType.Real - { "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier - { "smalldatetime", true }, // SqlDbType.SmallDateTime - { "smallint", true }, // SqlDbType.SmallInt - { "smallmoney", true }, // SqlDbType.SmallMoney - { "text", true }, // SqlDbType.Text - { "timestamp", true }, // SqlDbType.Timestamp - { "tinyint", true }, // SqlDbType.TinyInt - { "varbinary", true }, // SqlDbType.VarBinary - { "varchar", true }, // SqlDbType.VarChar - { "sql_variant", false }, // SqlDbType.Variant (unsupported) - { "xml", false }, // SqlDbType.Xml (unsupported) - { "date", true }, // SqlDbType.Date - { "time", true }, // SqlDbType.Time - { "datetime2", true }, // SqlDbType.DateTime2 - { "datetimeoffset", true }, // SqlDbType.DateTimeOffset - { "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported) - }; + { "bigint", true }, // SqlDbType.BigInt + { "binary", true }, // SqlDbType.Binary + { "bit", true }, // SqlDbType.Bit + { "char", true }, // SqlDbType.Char + { "datetime", true }, // SqlDbType.DateTime + { "decimal", true }, // SqlDbType.Decimal + { "float", true }, // SqlDbType.Float + { "image", true }, // SqlDbType.Image + { "int", true }, // SqlDbType.Int + { "money", true }, // SqlDbType.Money + { "nchar", true }, // SqlDbType.NChar + { "ntext", true }, // SqlDbType.NText + { "nvarchar", true }, // SqlDbType.NVarChar + { "real", true }, // SqlDbType.Real + { "uniqueidentifier", true }, // SqlDbType.UniqueIdentifier + { "smalldatetime", true }, // SqlDbType.SmallDateTime + { "smallint", true }, // SqlDbType.SmallInt + { "smallmoney", true }, // SqlDbType.SmallMoney + { "text", true }, // SqlDbType.Text + { "timestamp", true }, // SqlDbType.Timestamp + { "tinyint", true }, // SqlDbType.TinyInt + { "varbinary", true }, // SqlDbType.VarBinary + { "varchar", true }, // SqlDbType.VarChar + { "sql_variant", false }, // SqlDbType.Variant (unsupported) + { "xml", false }, // SqlDbType.Xml (unsupported) + { "date", true }, // SqlDbType.Date + { "time", true }, // SqlDbType.Time + { "datetime2", true }, // SqlDbType.DateTime2 + { "datetimeoffset", true }, // SqlDbType.DateTimeOffset + { "", false }, // SqlDbType.Udt and SqlDbType.Structured provided by SQL as empty strings (unsupported) + }; } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 14e73e19f1..aee27f3831 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -41,797 +41,797 @@ using VerifyMSTest; using static Azure.DataApiBuilder.Config.RuntimeConfigLoader; -namespace Azure.DataApiBuilder.Service.Tests.Configuration; - -[TestClass] -public class ConfigurationTests - : VerifyBase +namespace Azure.DataApiBuilder.Service.Tests.Configuration { - private const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL; - private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; - private const string MYSQL_ENVIRONMENT = TestCategory.MYSQL; - private const string POSTGRESQL_ENVIRONMENT = TestCategory.POSTGRESQL; - private const string POST_STARTUP_CONFIG_ENTITY = "Book"; - private const string POST_STARTUP_CONFIG_ENTITY_SOURCE = "books"; - private const string POST_STARTUP_CONFIG_ROLE = "PostStartupConfigRole"; - private const string COSMOS_DATABASE_NAME = "config_db"; - private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; - private const string OPENAPI_SWAGGER_ENDPOINT = "swagger"; - private const string OPENAPI_DOCUMENT_ENDPOINT = "openapi"; - private const string BROWSER_USER_AGENT_HEADER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"; - private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; - - private const int RETRY_COUNT = 5; - private const int RETRY_WAIT_SECONDS = 1; - - // TODO: Remove the old endpoint once we've updated all callers to use the new one. - private const string CONFIGURATION_ENDPOINT = "/configuration"; - private const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2"; - - /// - /// A valid REST API request body with correct parameter types for all the fields. - /// - public const string REQUEST_BODY_WITH_CORRECT_PARAM_TYPES = @" + [TestClass] + public class ConfigurationTests + : VerifyBase + { + private const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL; + private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL; + private const string MYSQL_ENVIRONMENT = TestCategory.MYSQL; + private const string POSTGRESQL_ENVIRONMENT = TestCategory.POSTGRESQL; + private const string POST_STARTUP_CONFIG_ENTITY = "Book"; + private const string POST_STARTUP_CONFIG_ENTITY_SOURCE = "books"; + private const string POST_STARTUP_CONFIG_ROLE = "PostStartupConfigRole"; + private const string COSMOS_DATABASE_NAME = "config_db"; + private const string CUSTOM_CONFIG_FILENAME = "custom-config.json"; + private const string OPENAPI_SWAGGER_ENDPOINT = "swagger"; + private const string OPENAPI_DOCUMENT_ENDPOINT = "openapi"; + private const string BROWSER_USER_AGENT_HEADER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"; + private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; + + private const int RETRY_COUNT = 5; + private const int RETRY_WAIT_SECONDS = 1; + + // TODO: Remove the old endpoint once we've updated all callers to use the new one. + private const string CONFIGURATION_ENDPOINT = "/configuration"; + private const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2"; + + /// + /// A valid REST API request body with correct parameter types for all the fields. + /// + public const string REQUEST_BODY_WITH_CORRECT_PARAM_TYPES = @" { ""title"": ""New book"", ""publisher_id"": 1234 } "; - /// - /// An invalid REST API request body with incorrect parameter type for publisher_id field. - /// - public const string REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES = @" + /// + /// An invalid REST API request body with incorrect parameter type for publisher_id field. + /// + public const string REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES = @" { ""title"": ""New book"", ""publisher_id"": ""one"" } "; - [TestCleanup] - public void CleanupAfterEachTest() - { - TestHelper.UnsetAllDABEnvironmentVariables(); - } - - /// - /// When updating config during runtime is possible, then For invalid config the Application continues to - /// accept request with status code of 503. - /// But if invalid config is provided during startup, ApplicationException is thrown - /// and application exits. - /// - [DataTestMethod] - [DataRow(new string[] { }, true, DisplayName = "No config returns 503 - config file flag absent")] - [DataRow(new string[] { "--ConfigFileName=" }, true, DisplayName = "No config returns 503 - empty config file option")] - [DataRow(new string[] { }, false, DisplayName = "Throws Application exception")] - [TestMethod("Validates that queries before runtime is configured returns a 503 in hosting scenario whereas an application exception when run through CLI")] - public async Task TestNoConfigReturnsServiceUnavailable( - string[] args, - bool isUpdateableRuntimeConfig) - { - TestServer server; + [TestCleanup] + public void CleanupAfterEachTest() + { + TestHelper.UnsetAllDABEnvironmentVariables(); + } - try + /// + /// When updating config during runtime is possible, then For invalid config the Application continues to + /// accept request with status code of 503. + /// But if invalid config is provided during startup, ApplicationException is thrown + /// and application exits. + /// + [DataTestMethod] + [DataRow(new string[] { }, true, DisplayName = "No config returns 503 - config file flag absent")] + [DataRow(new string[] { "--ConfigFileName=" }, true, DisplayName = "No config returns 503 - empty config file option")] + [DataRow(new string[] { }, false, DisplayName = "Throws Application exception")] + [TestMethod("Validates that queries before runtime is configured returns a 503 in hosting scenario whereas an application exception when run through CLI")] + public async Task TestNoConfigReturnsServiceUnavailable( + string[] args, + bool isUpdateableRuntimeConfig) { - if (isUpdateableRuntimeConfig) + TestServer server; + + try { - server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(args)); + if (isUpdateableRuntimeConfig) + { + server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(args)); + } + else + { + server = new(Program.CreateWebHostBuilder(args)); + } + + HttpClient httpClient = server.CreateClient(); + HttpResponseMessage result = await httpClient.GetAsync("/graphql"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, result.StatusCode); } - else + catch (Exception e) { - server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(isUpdateableRuntimeConfig); + Assert.AreEqual(typeof(ApplicationException), e.GetType()); + Assert.AreEqual( + $"Could not initialize the engine with the runtime config file: {DEFAULT_CONFIG_FILE_NAME}", + e.Message); } - - HttpClient httpClient = server.CreateClient(); - HttpResponseMessage result = await httpClient.GetAsync("/graphql"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, result.StatusCode); - } - catch (Exception e) - { - Assert.IsFalse(isUpdateableRuntimeConfig); - Assert.AreEqual(typeof(ApplicationException), e.GetType()); - Assert.AreEqual( - $"Could not initialize the engine with the runtime config file: {DEFAULT_CONFIG_FILE_NAME}", - e.Message); } - } - - /// - /// Verify that https redirection is disabled when --no-https-redirect flag is passed through CLI. - /// We check if IsHttpsRedirectionDisabled is set to true with --no-https-redirect flag. - /// - [DataTestMethod] - [DataRow(new string[] { "" }, false, DisplayName = "Https redirection allowed")] - [DataRow(new string[] { Startup.NO_HTTPS_REDIRECT_FLAG }, true, DisplayName = "Http redirection disabled")] - [TestMethod("Validates that https redirection is disabled when --no-https-redirect option is used when engine is started through CLI")] - public void TestDisablingHttpsRedirection( - string[] args, - bool expectedIsHttpsRedirectionDisabled) - { - Program.CreateWebHostBuilder(args).Build(); - Assert.AreEqual(expectedIsHttpsRedirectionDisabled, Program.IsHttpsRedirectionDisabled); - } - /// - /// Checks correct serialization and deserialization of Source Type from - /// Enum to String and vice-versa. - /// Consider both cases for source as an object and as a string - /// - [DataTestMethod] - [DataRow(true, EntitySourceType.StoredProcedure, "stored-procedure", DisplayName = "source is a stored-procedure")] - [DataRow(true, EntitySourceType.Table, "table", DisplayName = "source is a table")] - [DataRow(true, EntitySourceType.View, "view", DisplayName = "source is a view")] - [DataRow(false, null, null, DisplayName = "source is just string")] - public void TestCorrectSerializationOfSourceObject( - bool isDatabaseObjectSource, - EntitySourceType sourceObjectType, - string sourceTypeName) - { - RuntimeConfig runtimeConfig; - if (isDatabaseObjectSource) + /// + /// Verify that https redirection is disabled when --no-https-redirect flag is passed through CLI. + /// We check if IsHttpsRedirectionDisabled is set to true with --no-https-redirect flag. + /// + [DataTestMethod] + [DataRow(new string[] { "" }, false, DisplayName = "Https redirection allowed")] + [DataRow(new string[] { Startup.NO_HTTPS_REDIRECT_FLAG }, true, DisplayName = "Http redirection disabled")] + [TestMethod("Validates that https redirection is disabled when --no-https-redirect option is used when engine is started through CLI")] + public void TestDisablingHttpsRedirection( + string[] args, + bool expectedIsHttpsRedirectionDisabled) { - EntitySource entitySource = new( - Type: sourceObjectType, - Object: "sourceName", - Parameters: null, - KeyFields: null - ); - runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( - entityName: "MyEntity", - entitySource: entitySource, - roleName: "Anonymous", - operation: EntityActionOperation.All - ); + Program.CreateWebHostBuilder(args).Build(); + Assert.AreEqual(expectedIsHttpsRedirectionDisabled, Program.IsHttpsRedirectionDisabled); } - else + + /// + /// Checks correct serialization and deserialization of Source Type from + /// Enum to String and vice-versa. + /// Consider both cases for source as an object and as a string + /// + [DataTestMethod] + [DataRow(true, EntitySourceType.StoredProcedure, "stored-procedure", DisplayName = "source is a stored-procedure")] + [DataRow(true, EntitySourceType.Table, "table", DisplayName = "source is a table")] + [DataRow(true, EntitySourceType.View, "view", DisplayName = "source is a view")] + [DataRow(false, null, null, DisplayName = "source is just string")] + public void TestCorrectSerializationOfSourceObject( + bool isDatabaseObjectSource, + EntitySourceType sourceObjectType, + string sourceTypeName) { - string entitySource = "sourceName"; - runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( - entityName: "MyEntity", - entitySource: entitySource, - roleName: "Anonymous", - operation: EntityActionOperation.All - ); - } + RuntimeConfig runtimeConfig; + if (isDatabaseObjectSource) + { + EntitySource entitySource = new( + Type: sourceObjectType, + Object: "sourceName", + Parameters: null, + KeyFields: null + ); + runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( + entityName: "MyEntity", + entitySource: entitySource, + roleName: "Anonymous", + operation: EntityActionOperation.All + ); + } + else + { + string entitySource = "sourceName"; + runtimeConfig = AuthorizationHelpers.InitRuntimeConfig( + entityName: "MyEntity", + entitySource: entitySource, + roleName: "Anonymous", + operation: EntityActionOperation.All + ); + } - string runtimeConfigJson = runtimeConfig.ToJson(); + string runtimeConfigJson = runtimeConfig.ToJson(); - if (isDatabaseObjectSource) - { - Assert.IsTrue(runtimeConfigJson.Contains(sourceTypeName)); - } + if (isDatabaseObjectSource) + { + Assert.IsTrue(runtimeConfigJson.Contains(sourceTypeName)); + } - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(runtimeConfigJson, out RuntimeConfig deserializedRuntimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(runtimeConfigJson, out RuntimeConfig deserializedRuntimeConfig)); - Assert.IsTrue(deserializedRuntimeConfig.Entities.ContainsKey("MyEntity")); - Assert.AreEqual("sourceName", deserializedRuntimeConfig.Entities["MyEntity"].Source.Object); + Assert.IsTrue(deserializedRuntimeConfig.Entities.ContainsKey("MyEntity")); + Assert.AreEqual("sourceName", deserializedRuntimeConfig.Entities["MyEntity"].Source.Object); - if (isDatabaseObjectSource) - { - Assert.AreEqual(sourceObjectType, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); + if (isDatabaseObjectSource) + { + Assert.AreEqual(sourceObjectType, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); + } + else + { + Assert.AreEqual(EntitySourceType.Table, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); + } } - else + + [TestMethod("Validates that once the configuration is set, the config controller isn't reachable."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestConflictAlreadySetConfiguration(string configurationEndpoint) { - Assert.AreEqual(EntitySourceType.Table, deserializedRuntimeConfig.Entities["MyEntity"].Source.Type); - } - } + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates that once the configuration is set, the config controller isn't reachable."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestConflictAlreadySetConfiguration(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + _ = await httpClient.PostAsync(configurationEndpoint, content); + ValidateCosmosDbSetup(server); - _ = await httpClient.PostAsync(configurationEndpoint, content); - ValidateCosmosDbSetup(server); + HttpResponseMessage result = await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); + } - HttpResponseMessage result = await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); - } + [TestMethod("Validates that the config controller returns a conflict when using local configuration."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestConflictLocalConfiguration(string configurationEndpoint) + { + Environment.SetEnvironmentVariable + (ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates that the config controller returns a conflict when using local configuration."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestConflictLocalConfiguration(string configurationEndpoint) - { - Environment.SetEnvironmentVariable - (ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + ValidateCosmosDbSetup(server); - ValidateCosmosDbSetup(server); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + HttpResponseMessage result = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); + } - HttpResponseMessage result = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.Conflict, result.StatusCode); - } + [TestMethod("Validates setting the configuration at runtime."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates setting the configuration at runtime."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + } - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - } + [TestMethod("Validates an invalid configuration returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestInvalidConfigurationAtRuntime(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates an invalid configuration returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestInvalidConfigurationAtRuntime(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, "invalidString"); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, "invalidString"); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); + } - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); - } + [TestMethod("Validates a failure in one of the config updated handlers returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingFailureConfigurations(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - [TestMethod("Validates a failure in one of the config updated handlers returns a bad request."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingFailureConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + + RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); + runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((_, _) => + { + return Task.FromResult(false); + }); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); - RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); - runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add((_, _) => + Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); + } + + [TestMethod("Validates that the configuration endpoint doesn't return until all configuration loaded handlers have executed."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestLongRunningConfigUpdatedHandlerConfigurations(string configurationEndpoint) { - return Task.FromResult(false); - }); + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - Assert.AreEqual(HttpStatusCode.BadRequest, postResult.StatusCode); - } + RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); + bool taskHasCompleted = false; + runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) => + { + await Task.Delay(1000); + taskHasCompleted = true; + return true; + }); - [TestMethod("Validates that the configuration endpoint doesn't return until all configuration loaded handlers have executed."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestLongRunningConfigUpdatedHandlerConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + Assert.IsTrue(taskHasCompleted); + } - RuntimeConfigProvider runtimeConfigProvider = server.Services.GetService(); - bool taskHasCompleted = false; - runtimeConfigProvider.RuntimeConfigLoadedHandlers.Add(async (_, _) => + /// + /// Tests that sending configuration to the DAB engine post-startup will properly hydrate + /// the AuthorizationResolver by: + /// 1. Validate that pre-configuration hydration requests result in 503 Service Unavailable + /// 2. Validate that custom configuration hydration succeeds. + /// 3. Validate that request to protected entity without role membership triggers Authorization Resolver + /// to reject the request with HTTP 403 Forbidden. + /// 4. Validate that request to protected entity with required role membership passes authorization requirements + /// and succeeds with HTTP 200 OK. + /// Note: This test is database engine agnostic, though requires denoting a database environment to fetch a usable + /// connection string to complete the test. Most applicable to CI/CD test execution. + /// + [TestCategory(TestCategory.MSSQL)] + [TestMethod("Validates setting the AuthN/Z configuration post-startup during runtime.")] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSqlSettingPostStartupConfigurations(string configurationEndpoint) { - await Task.Delay(1000); - taskHasCompleted = true; - return true; - }); + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); + RuntimeConfig configuration = AuthorizationHelpers.InitRuntimeConfig( + entityName: POST_STARTUP_CONFIG_ENTITY, + entitySource: POST_STARTUP_CONFIG_ENTITY_SOURCE, + roleName: POST_STARTUP_CONFIG_ROLE, + operation: EntityActionOperation.Read, + includedCols: new HashSet() { "*" }); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - Assert.IsTrue(taskHasCompleted); - } + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - /// - /// Tests that sending configuration to the DAB engine post-startup will properly hydrate - /// the AuthorizationResolver by: - /// 1. Validate that pre-configuration hydration requests result in 503 Service Unavailable - /// 2. Validate that custom configuration hydration succeeds. - /// 3. Validate that request to protected entity without role membership triggers Authorization Resolver - /// to reject the request with HTTP 403 Forbidden. - /// 4. Validate that request to protected entity with required role membership passes authorization requirements - /// and succeeds with HTTP 200 OK. - /// Note: This test is database engine agnostic, though requires denoting a database environment to fetch a usable - /// connection string to complete the test. Most applicable to CI/CD test execution. - /// - [TestCategory(TestCategory.MSSQL)] - [TestMethod("Validates setting the AuthN/Z configuration post-startup during runtime.")] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSqlSettingPostStartupConfigurations(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); - - RuntimeConfig configuration = AuthorizationHelpers.InitRuntimeConfig( - entityName: POST_STARTUP_CONFIG_ENTITY, - entitySource: POST_STARTUP_CONFIG_ENTITY_SOURCE, - roleName: POST_STARTUP_CONFIG_ROLE, - operation: EntityActionOperation.Read, - includedCols: new HashSet() { "*" }); - - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - - HttpResponseMessage preConfigHydrationResult = - await httpClient.GetAsync($"/{POST_STARTUP_CONFIG_ENTITY}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigHydrationResult.StatusCode); - - HttpResponseMessage preConfigOpenApiDocumentExistence = - await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiDocumentExistence.StatusCode); - - // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. - HttpResponseMessage preConfigOpenApiSwaggerEndpointAvailability = - await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); - - HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); - - // When the authorization resolver is properly configured, authorization will have failed - // because no auth headers are present. - Assert.AreEqual( - expected: HttpStatusCode.Forbidden, - actual: responseCode, - message: "Configuration not yet hydrated after retry attempts.."); - - // Sends a GET request to a protected entity which requires a specific role to access. - // Authorization will pass because proper auth headers are present. - HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"api/{POST_STARTUP_CONFIG_ENTITY}"); - string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - specificRole: POST_STARTUP_CONFIG_ROLE); - message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); - message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); - HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); - Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); - - // OpenAPI document is created during config hydration and - // is made available after config hydration completes. - HttpResponseMessage postConfigOpenApiDocumentExistence = - await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.OK, postConfigOpenApiDocumentExistence.StatusCode); - - // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. - // HTTP 400 - BadRequest because when SwaggerUI is disabled, the endpoint is not mapped - // and the request is processed and failed by the RestService. - HttpResponseMessage postConfigOpenApiSwaggerEndpointAvailability = - await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); - Assert.AreEqual(HttpStatusCode.BadRequest, postConfigOpenApiSwaggerEndpointAvailability.StatusCode); - } + HttpResponseMessage preConfigHydrationResult = + await httpClient.GetAsync($"/{POST_STARTUP_CONFIG_ENTITY}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigHydrationResult.StatusCode); - [TestMethod("Validates that local CosmosDB_NoSQL settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestLoadingLocalCosmosSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpResponseMessage preConfigOpenApiDocumentExistence = + await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiDocumentExistence.StatusCode); - ValidateCosmosDbSetup(server); - } + // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. + HttpResponseMessage preConfigOpenApiSwaggerEndpointAvailability = + await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); - [TestMethod("Validates access token is correctly loaded when Account Key is not present for Cosmos."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestLoadingAccessTokenForCosmosClient(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient httpClient = server.CreateClient(); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, null, true); + // When the authorization resolver is properly configured, authorization will have failed + // because no auth headers are present. + Assert.AreEqual( + expected: HttpStatusCode.Forbidden, + actual: responseCode, + message: "Configuration not yet hydrated after retry attempts.."); + + // Sends a GET request to a protected entity which requires a specific role to access. + // Authorization will pass because proper auth headers are present. + HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"api/{POST_STARTUP_CONFIG_ENTITY}"); + string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( + addAuthenticated: true, + specificRole: POST_STARTUP_CONFIG_ROLE); + message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); + message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); + HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); + + // OpenAPI document is created during config hydration and + // is made available after config hydration completes. + HttpResponseMessage postConfigOpenApiDocumentExistence = + await httpClient.GetAsync($"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.OK, postConfigOpenApiDocumentExistence.StatusCode); + + // SwaggerUI (OpenAPI user interface) is not made available in production/hosting mode. + // HTTP 400 - BadRequest because when SwaggerUI is disabled, the endpoint is not mapped + // and the request is processed and failed by the RestService. + HttpResponseMessage postConfigOpenApiSwaggerEndpointAvailability = + await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); + Assert.AreEqual(HttpStatusCode.BadRequest, postConfigOpenApiSwaggerEndpointAvailability.StatusCode); + } - HttpResponseMessage authorizedResponse = await httpClient.PostAsync(configurationEndpoint, content); + [TestMethod("Validates that local CosmosDB_NoSQL settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestLoadingLocalCosmosSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); - CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.IsNotNull(cosmosClientProvider); - Assert.IsNotNull(cosmosClientProvider.Client); - } + ValidateCosmosDbSetup(server); + } - [TestMethod("Validates that local MsSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MSSQL)] - public void TestLoadingLocalMsSqlSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + [TestMethod("Validates access token is correctly loaded when Account Key is not present for Cosmos."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestLoadingAccessTokenForCosmosClient(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient httpClient = server.CreateClient(); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint, null, true); - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + HttpResponseMessage authorizedResponse = await httpClient.PostAsync(configurationEndpoint, content); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(MsSqlQueryBuilder)); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); + CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.IsNotNull(cosmosClientProvider); + Assert.IsNotNull(cosmosClientProvider.Client); + } - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + [TestMethod("Validates that local MsSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MSSQL)] + public void TestLoadingLocalMsSqlSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MsSqlMetadataProvider)); - } + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - [TestMethod("Validates that local PostgreSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.POSTGRESQL)] - public void TestLoadingLocalPostgresSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, POSTGRESQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(MsSqlQueryBuilder)); - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(PostgresQueryBuilder)); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MsSqlMetadataProvider)); + } - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + [TestMethod("Validates that local PostgreSql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.POSTGRESQL)] + public void TestLoadingLocalPostgresSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, POSTGRESQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(PostgreSqlMetadataProvider)); - } + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - [TestMethod("Validates that local MySql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MYSQL)] - public void TestLoadingLocalMySqlSettings() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MYSQL_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(PostgresQueryBuilder)); - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); - Assert.IsInstanceOfType(queryBuilder, typeof(MySqlQueryBuilder)); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(PostgreSqlMetadataProvider)); + } - object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); - Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); + [TestMethod("Validates that local MySql settings can be loaded and the correct classes are in the service provider."), TestCategory(TestCategory.MYSQL)] + public void TestLoadingLocalMySqlSettings() + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MYSQL_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MySqlMetadataProvider)); - } + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(SqlQueryEngine)); - [TestMethod("Validates that trying to override configs that are already set fail."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestOverridingLocalSettingsFails(string configurationEndpoint) - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - HttpClient client = server.CreateClient(); + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(SqlMutationEngine)); - JsonContent config = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + object queryBuilder = server.Services.GetService(typeof(IQueryBuilder)); + Assert.IsInstanceOfType(queryBuilder, typeof(MySqlQueryBuilder)); - HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, config); - Assert.AreEqual(HttpStatusCode.Conflict, postResult.StatusCode); - } + object queryExecutor = server.Services.GetService(typeof(IQueryExecutor)); + Assert.IsInstanceOfType(queryExecutor, typeof(QueryExecutor)); - [TestMethod("Validates that setting the configuration at runtime will instantiate the proper classes."), TestCategory(TestCategory.COSMOSDBNOSQL)] - [DataRow(CONFIGURATION_ENDPOINT)] - [DataRow(CONFIGURATION_ENDPOINT_V2)] - public async Task TestSettingConfigurationCreatesCorrectClasses(string configurationEndpoint) - { - TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); - HttpClient client = server.CreateClient(); + object sqlMetadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(sqlMetadataProvider, typeof(MySqlMetadataProvider)); + } - JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); + [TestMethod("Validates that trying to override configs that are already set fail."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestOverridingLocalSettingsFails(string configurationEndpoint) + { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + HttpClient client = server.CreateClient(); - HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + JsonContent config = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - ValidateCosmosDbSetup(server); - RuntimeConfigProvider configProvider = server.Services.GetService(); + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, config); + Assert.AreEqual(HttpStatusCode.Conflict, postResult.StatusCode); + } - Assert.IsNotNull(configProvider, "Configuration Provider shouldn't be null after setting the configuration at runtime."); - Assert.IsTrue(configProvider.TryGetConfig(out RuntimeConfig configuration), "TryGetConfig should return true when the config is set."); - Assert.IsNotNull(configuration, "Config returned should not be null."); + [TestMethod("Validates that setting the configuration at runtime will instantiate the proper classes."), TestCategory(TestCategory.COSMOSDBNOSQL)] + [DataRow(CONFIGURATION_ENDPOINT)] + [DataRow(CONFIGURATION_ENDPOINT_V2)] + public async Task TestSettingConfigurationCreatesCorrectClasses(string configurationEndpoint) + { + TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty())); + HttpClient client = server.CreateClient(); - ConfigurationPostParameters expectedParameters = GetCosmosConfigurationParameters(); - Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, configuration.DataSource.DatabaseType, "Expected CosmosDB_NoSQL database type after configuring the runtime with CosmosDB_NoSQL settings."); - Assert.AreEqual(expectedParameters.Schema, configuration.DataSource.GetTypedOptions().GraphQLSchema, "Expected the schema in the configuration to match the one sent to the configuration endpoint."); + JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint); - // Don't use Assert.AreEqual, because a failure will print the entire connection string in the error message. - Assert.IsTrue(expectedParameters.ConnectionString == configuration.DataSource.ConnectionString, "Expected the connection string in the configuration to match the one sent to the configuration endpoint."); - string db = configuration.DataSource.GetTypedOptions().Database; - Assert.AreEqual(COSMOS_DATABASE_NAME, db, "Expected the database name in the runtime config to match the one sent to the configuration endpoint."); - } + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - [TestMethod("Validates that an exception is thrown if there's a null model in filter parser.")] - public void VerifyExceptionOnNullModelinFilterParser() - { - ODataParser parser = new(); - try - { - // FilterParser has no model so we expect exception - parser.GetFilterClause(filterQueryString: string.Empty, resourcePath: string.Empty); - Assert.Fail(); + ValidateCosmosDbSetup(server); + RuntimeConfigProvider configProvider = server.Services.GetService(); + + Assert.IsNotNull(configProvider, "Configuration Provider shouldn't be null after setting the configuration at runtime."); + Assert.IsTrue(configProvider.TryGetConfig(out RuntimeConfig configuration), "TryGetConfig should return true when the config is set."); + Assert.IsNotNull(configuration, "Config returned should not be null."); + + ConfigurationPostParameters expectedParameters = GetCosmosConfigurationParameters(); + Assert.AreEqual(DatabaseType.CosmosDB_NoSQL, configuration.DataSource.DatabaseType, "Expected CosmosDB_NoSQL database type after configuring the runtime with CosmosDB_NoSQL settings."); + Assert.AreEqual(expectedParameters.Schema, configuration.DataSource.GetTypedOptions().GraphQLSchema, "Expected the schema in the configuration to match the one sent to the configuration endpoint."); + + // Don't use Assert.AreEqual, because a failure will print the entire connection string in the error message. + Assert.IsTrue(expectedParameters.ConnectionString == configuration.DataSource.ConnectionString, "Expected the connection string in the configuration to match the one sent to the configuration endpoint."); + string db = configuration.DataSource.GetTypedOptions().Database; + Assert.AreEqual(COSMOS_DATABASE_NAME, db, "Expected the database name in the runtime config to match the one sent to the configuration endpoint."); } - catch (DataApiBuilderException exception) + + [TestMethod("Validates that an exception is thrown if there's a null model in filter parser.")] + public void VerifyExceptionOnNullModelinFilterParser() { - Assert.AreEqual("The runtime has not been initialized with an Edm model.", exception.Message); - Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); - Assert.AreEqual(DataApiBuilderException.SubStatusCodes.UnexpectedError, exception.SubStatusCode); + ODataParser parser = new(); + try + { + // FilterParser has no model so we expect exception + parser.GetFilterClause(filterQueryString: string.Empty, resourcePath: string.Empty); + Assert.Fail(); + } + catch (DataApiBuilderException exception) + { + Assert.AreEqual("The runtime has not been initialized with an Edm model.", exception.Message); + Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode); + Assert.AreEqual(DataApiBuilderException.SubStatusCodes.UnexpectedError, exception.SubStatusCode); + } } - } - /// - /// This test reads the dab-config.MsSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of MsSql config file succeeds."), TestCategory(TestCategory.MSSQL)] - public Task TestReadingRuntimeConfigForMsSql() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MSSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + /// + /// This test reads the dab-config.MsSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of MsSql config file succeeds."), TestCategory(TestCategory.MSSQL)] + public Task TestReadingRuntimeConfigForMsSql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MSSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - /// - /// This test reads the dab-config.MySql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of MySql config file succeeds."), TestCategory(TestCategory.MYSQL)] - public Task TestReadingRuntimeConfigForMySql() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MYSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + /// + /// This test reads the dab-config.MySql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of MySql config file succeeds."), TestCategory(TestCategory.MYSQL)] + public Task TestReadingRuntimeConfigForMySql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{MYSQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - /// - /// This test reads the dab-config.PostgreSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of PostgreSql config file succeeds."), TestCategory(TestCategory.POSTGRESQL)] - public Task TestReadingRuntimeConfigForPostgreSql() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{POSTGRESQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + /// + /// This test reads the dab-config.PostgreSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of PostgreSql config file succeeds."), TestCategory(TestCategory.POSTGRESQL)] + public Task TestReadingRuntimeConfigForPostgreSql() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{POSTGRESQL_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - /// - /// This test reads the dab-config.CosmosDb_NoSql.json file and validates that the - /// deserialization succeeds. - /// - [TestMethod("Validates if deserialization of the CosmosDB_NoSQL config file succeeds."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public Task TestReadingRuntimeConfigForCosmos() - { - return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); - } + /// + /// This test reads the dab-config.CosmosDb_NoSql.json file and validates that the + /// deserialization succeeds. + /// + [TestMethod("Validates if deserialization of the CosmosDB_NoSQL config file succeeds."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public Task TestReadingRuntimeConfigForCosmos() + { + return ConfigFileDeserializationValidationHelper(File.ReadAllText($"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}")); + } - /// - /// Helper method to validate the deserialization of the "entities" section of the config file - /// This is used in unit tests that validate the deserialization of the config files - /// - /// - private Task ConfigFileDeserializationValidationHelper(string jsonString) - { - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonString, out RuntimeConfig runtimeConfig), "Deserialization of the config file failed."); - return Verify(runtimeConfig); - } + /// + /// Helper method to validate the deserialization of the "entities" section of the config file + /// This is used in unit tests that validate the deserialization of the config files + /// + /// + private Task ConfigFileDeserializationValidationHelper(string jsonString) + { + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonString, out RuntimeConfig runtimeConfig), "Deserialization of the config file failed."); + return Verify(runtimeConfig); + } - /// - /// This function verifies command line configuration provider takes higher - /// precedence than default configuration file dab-config.json - /// - [TestMethod("Validates command line configuration provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestCommandLineConfigurationProvider() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - string[] args = new[] + /// + /// This function verifies command line configuration provider takes higher + /// precedence than default configuration file dab-config.json + /// + [TestMethod("Validates command line configuration provider."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestCommandLineConfigurationProvider() { + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + string[] args = new[] + { $"--ConfigFileName={RuntimeConfigLoader.CONFIGFILE_NAME}." + $"{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}" }; - TestServer server = new(Program.CreateWebHostBuilder(args)); - - ValidateCosmosDbSetup(server); - } - - /// - /// This function verifies the environment variable DAB_ENVIRONMENT - /// takes precedence than ASPNETCORE_ENVIRONMENT for the configuration file. - /// - [TestMethod("Validates precedence is given to DAB_ENVIRONMENT environment variable name."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestRuntimeEnvironmentVariable() - { - Environment.SetEnvironmentVariable( - ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); - Environment.SetEnvironmentVariable( - RuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + TestServer server = new(Program.CreateWebHostBuilder(args)); - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + ValidateCosmosDbSetup(server); + } - ValidateCosmosDbSetup(server); - } + /// + /// This function verifies the environment variable DAB_ENVIRONMENT + /// takes precedence than ASPNETCORE_ENVIRONMENT for the configuration file. + /// + [TestMethod("Validates precedence is given to DAB_ENVIRONMENT environment variable name."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestRuntimeEnvironmentVariable() + { + Environment.SetEnvironmentVariable( + ASP_NET_CORE_ENVIRONMENT_VAR_NAME, MSSQL_ENVIRONMENT); + Environment.SetEnvironmentVariable( + RuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - [TestMethod("Validates the runtime configuration file."), TestCategory(TestCategory.MSSQL)] - public void TestConfigIsValid() - { - TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); - RuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); - RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configPath); - - Mock> configValidatorLogger = new(); - IConfigValidator configValidator = - new RuntimeConfigValidator( - configProvider, - new MockFileSystem(), - configValidatorLogger.Object); - - configValidator.ValidateConfig(); - TestHelper.UnsetAllDABEnvironmentVariables(); - } + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - /// - /// Set the connection string to an invalid value and expect the service to be unavailable - /// since without this env var, it would be available - guaranteeing this env variable - /// has highest precedence irrespective of what the connection string is in the config file. - /// Verifying the Exception thrown. - /// - [TestMethod($"Validates that environment variable {RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} has highest precedence."), TestCategory(TestCategory.COSMOSDBNOSQL)] - public void TestConnectionStringEnvVarHasHighestPrecedence() - { - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); - Environment.SetEnvironmentVariable( - RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING, - "Invalid Connection String"); + ValidateCosmosDbSetup(server); + } - try + [TestMethod("Validates the runtime configuration file."), TestCategory(TestCategory.MSSQL)] + public void TestConfigIsValid() { - TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); - _ = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.Fail($"{RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} is not given highest precedence"); + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + RuntimeConfigLoader configPath = TestHelper.GetRuntimeConfigLoader(); + RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(configPath); + + Mock> configValidatorLogger = new(); + IConfigValidator configValidator = + new RuntimeConfigValidator( + configProvider, + new MockFileSystem(), + configValidatorLogger.Object); + + configValidator.ValidateConfig(); + TestHelper.UnsetAllDABEnvironmentVariables(); } - catch (Exception e) + + /// + /// Set the connection string to an invalid value and expect the service to be unavailable + /// since without this env var, it would be available - guaranteeing this env variable + /// has highest precedence irrespective of what the connection string is in the config file. + /// Verifying the Exception thrown. + /// + [TestMethod($"Validates that environment variable {RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} has highest precedence."), TestCategory(TestCategory.COSMOSDBNOSQL)] + public void TestConnectionStringEnvVarHasHighestPrecedence() { - Assert.AreEqual(typeof(ApplicationException), e.GetType()); - Assert.AreEqual( - $"Could not initialize the engine with the runtime config file: " + - $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}", - e.Message); - } - } + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, COSMOS_ENVIRONMENT); + Environment.SetEnvironmentVariable( + RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING, + "Invalid Connection String"); - /// - /// Test to verify the precedence logic for config file based on Environment variables. - /// - [DataTestMethod] - [DataRow("HostTest", "Test", false, $"{CONFIGFILE_NAME}.Test{CONFIG_EXTENSION}", DisplayName = "hosting and dab environment set, without considering overrides.")] - [DataRow("HostTest", "", false, $"{CONFIGFILE_NAME}.HostTest{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, without considering overrides.")] - [DataRow("", "Test1", false, $"{CONFIGFILE_NAME}.Test1{CONFIG_EXTENSION}", DisplayName = "only dab environment set, without considering overrides.")] - [DataRow("", "Test2", true, $"{CONFIGFILE_NAME}.Test2.overrides{CONFIG_EXTENSION}", DisplayName = "only dab environment set, considering overrides.")] - [DataRow("HostTest1", "", true, $"{CONFIGFILE_NAME}.HostTest1.overrides{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, considering overrides.")] - public void TestGetConfigFileNameForEnvironment( - string hostingEnvironmentValue, - string environmentValue, - bool considerOverrides, - string expectedRuntimeConfigFile) - { - MockFileSystem fileSystem = new(); - fileSystem.AddFile(expectedRuntimeConfigFile, new MockFileData(string.Empty)); - RuntimeConfigLoader runtimeConfigLoader = new(fileSystem); - - Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, hostingEnvironmentValue); - Environment.SetEnvironmentVariable(RUNTIME_ENVIRONMENT_VAR_NAME, environmentValue); - string actualRuntimeConfigFile = runtimeConfigLoader.GetFileNameForEnvironment(hostingEnvironmentValue, considerOverrides); - Assert.AreEqual(expectedRuntimeConfigFile, actualRuntimeConfigFile); - } + try + { + TestServer server = new(Program.CreateWebHostBuilder(Array.Empty())); + _ = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.Fail($"{RuntimeConfigLoader.RUNTIME_ENV_CONNECTION_STRING} is not given highest precedence"); + } + catch (Exception e) + { + Assert.AreEqual(typeof(ApplicationException), e.GetType()); + Assert.AreEqual( + $"Could not initialize the engine with the runtime config file: " + + $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}", + e.Message); + } + } - /// - /// Test different graphql endpoints in different host modes - /// when accessed interactively via browser. - /// - /// The endpoint route - /// The mode in which the service is executing. - /// Expected Status Code. - /// The expected phrase in the response body. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop", - DisplayName = "GraphQL endpoint with no query in development mode.")] - [DataRow("/graphql", HostMode.Production, HttpStatusCode.BadRequest, - "Either the parameter query or the parameter id has to be set", - DisplayName = "GraphQL endpoint with no query in production mode.")] - [DataRow("/graphql/ui", HostMode.Development, HttpStatusCode.NotFound, - DisplayName = "Default BananaCakePop in development mode.")] - [DataRow("/graphql/ui", HostMode.Production, HttpStatusCode.NotFound, - DisplayName = "Default BananaCakePop in production mode.")] - [DataRow("/graphql?query={book_by_pk(id: 1){title}}", - HostMode.Development, HttpStatusCode.Moved, - DisplayName = "GraphQL endpoint with query in development mode.")] - [DataRow("/graphql?query={book_by_pk(id: 1){title}}", - HostMode.Production, HttpStatusCode.OK, "data", - DisplayName = "GraphQL endpoint with query in production mode.")] - [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Development, HttpStatusCode.BadRequest, - "GraphQL request redirected to favicon.ico.", - DisplayName = "Redirected endpoint in development mode.")] - [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Production, HttpStatusCode.BadRequest, - "GraphQL request redirected to favicon.ico.", - DisplayName = "Redirected endpoint in production mode.")] - public async Task TestInteractiveGraphQLEndpoints( - string endpoint, - HostMode HostMode, - HttpStatusCode expectedStatusCode, - string expectedContent = "") - { - const string CUSTOM_CONFIG = "custom-config.json"; - TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); - FileSystem fileSystem = new(); - RuntimeConfigLoader loader = new(fileSystem); - loader.TryLoadKnownConfig(out RuntimeConfig config); + /// + /// Test to verify the precedence logic for config file based on Environment variables. + /// + [DataTestMethod] + [DataRow("HostTest", "Test", false, $"{CONFIGFILE_NAME}.Test{CONFIG_EXTENSION}", DisplayName = "hosting and dab environment set, without considering overrides.")] + [DataRow("HostTest", "", false, $"{CONFIGFILE_NAME}.HostTest{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, without considering overrides.")] + [DataRow("", "Test1", false, $"{CONFIGFILE_NAME}.Test1{CONFIG_EXTENSION}", DisplayName = "only dab environment set, without considering overrides.")] + [DataRow("", "Test2", true, $"{CONFIGFILE_NAME}.Test2.overrides{CONFIG_EXTENSION}", DisplayName = "only dab environment set, considering overrides.")] + [DataRow("HostTest1", "", true, $"{CONFIGFILE_NAME}.HostTest1.overrides{CONFIG_EXTENSION}", DisplayName = "only hosting environment set, considering overrides.")] + public void TestGetConfigFileNameForEnvironment( + string hostingEnvironmentValue, + string environmentValue, + bool considerOverrides, + string expectedRuntimeConfigFile) + { + MockFileSystem fileSystem = new(); + fileSystem.AddFile(expectedRuntimeConfigFile, new MockFileData(string.Empty)); + RuntimeConfigLoader runtimeConfigLoader = new(fileSystem); + + Environment.SetEnvironmentVariable(ASP_NET_CORE_ENVIRONMENT_VAR_NAME, hostingEnvironmentValue); + Environment.SetEnvironmentVariable(RUNTIME_ENVIRONMENT_VAR_NAME, environmentValue); + string actualRuntimeConfigFile = runtimeConfigLoader.GetFileNameForEnvironment(hostingEnvironmentValue, considerOverrides); + Assert.AreEqual(expectedRuntimeConfigFile, actualRuntimeConfigFile); + } - RuntimeConfig configWithCustomHostMode = config with + /// + /// Test different graphql endpoints in different host modes + /// when accessed interactively via browser. + /// + /// The endpoint route + /// The mode in which the service is executing. + /// Expected Status Code. + /// The expected phrase in the response body. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/graphql/", HostMode.Development, HttpStatusCode.OK, "Banana Cake Pop", + DisplayName = "GraphQL endpoint with no query in development mode.")] + [DataRow("/graphql", HostMode.Production, HttpStatusCode.BadRequest, + "Either the parameter query or the parameter id has to be set", + DisplayName = "GraphQL endpoint with no query in production mode.")] + [DataRow("/graphql/ui", HostMode.Development, HttpStatusCode.NotFound, + DisplayName = "Default BananaCakePop in development mode.")] + [DataRow("/graphql/ui", HostMode.Production, HttpStatusCode.NotFound, + DisplayName = "Default BananaCakePop in production mode.")] + [DataRow("/graphql?query={book_by_pk(id: 1){title}}", + HostMode.Development, HttpStatusCode.Moved, + DisplayName = "GraphQL endpoint with query in development mode.")] + [DataRow("/graphql?query={book_by_pk(id: 1){title}}", + HostMode.Production, HttpStatusCode.OK, "data", + DisplayName = "GraphQL endpoint with query in production mode.")] + [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Development, HttpStatusCode.BadRequest, + "GraphQL request redirected to favicon.ico.", + DisplayName = "Redirected endpoint in development mode.")] + [DataRow(RestController.REDIRECTED_ROUTE, HostMode.Production, HttpStatusCode.BadRequest, + "GraphQL request redirected to favicon.ico.", + DisplayName = "Redirected endpoint in production mode.")] + public async Task TestInteractiveGraphQLEndpoints( + string endpoint, + HostMode HostMode, + HttpStatusCode expectedStatusCode, + string expectedContent = "") { - Runtime = config.Runtime with + const string CUSTOM_CONFIG = "custom-config.json"; + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + FileSystem fileSystem = new(); + RuntimeConfigLoader loader = new(fileSystem); + loader.TryLoadKnownConfig(out RuntimeConfig config); + + RuntimeConfig configWithCustomHostMode = config with + { + Runtime = config.Runtime with + { + Host = config.Runtime.Host with { Mode = HostMode } + } + }; + File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); + string[] args = new[] { - Host = config.Runtime.Host with { Mode = HostMode } - } - }; - File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); - string[] args = new[] - { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - { - HttpRequestMessage request = new(HttpMethod.Get, endpoint); + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + { + HttpRequestMessage request = new(HttpMethod.Get, endpoint); - // Adding the following headers simulates an interactive browser request. - request.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); - request.Headers.Add("accept", BROWSER_ACCEPT_HEADER); + // Adding the following headers simulates an interactive browser request. + request.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); + request.Headers.Add("accept", BROWSER_ACCEPT_HEADER); - HttpResponseMessage response = await client.SendAsync(request); - Assert.AreEqual(expectedStatusCode, response.StatusCode); - string actualBody = await response.Content.ReadAsStringAsync(); - Assert.IsTrue(actualBody.Contains(expectedContent)); + HttpResponseMessage response = await client.SendAsync(request); + Assert.AreEqual(expectedStatusCode, response.StatusCode); + string actualBody = await response.Content.ReadAsStringAsync(); + Assert.IsTrue(actualBody.Contains(expectedContent)); - TestHelper.UnsetAllDABEnvironmentVariables(); + TestHelper.UnsetAllDABEnvironmentVariables(); + } } - } - /// - /// Tests that the custom path rewriting middleware properly rewrites the - /// first segment of a path (/segment1/.../segmentN) when the segment matches - /// the custom configured GraphQLEndpoint. - /// Note: The GraphQL service is always internally mapped to /graphql - /// - /// The custom configured GraphQL path in configuration - /// The path used in the web request executed in the test. - /// Expected Http success/error code - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/graphql", "/gql", HttpStatusCode.BadRequest, DisplayName = "Request to non-configured graphQL endpoint is handled by REST controller.")] - [DataRow("/graphql", "/graphql", HttpStatusCode.OK, DisplayName = "Request to configured default GraphQL endpoint succeeds, path not rewritten.")] - [DataRow("/gql", "/gql/additionalURLsegment", HttpStatusCode.OK, DisplayName = "GraphQL request path (with extra segments) rewritten to match internally set GraphQL endpoint /graphql.")] - [DataRow("/gql", "/gql", HttpStatusCode.OK, DisplayName = "GraphQL request path rewritten to match internally set GraphQL endpoint /graphql.")] - [DataRow("/gql", "/api/book", HttpStatusCode.NotFound, DisplayName = "Non-GraphQL request's path is not rewritten and is handled by REST controller.")] - [DataRow("/gql", "/graphql", HttpStatusCode.NotFound, DisplayName = "Requests to default/internally set graphQL endpoint fail when configured endpoint differs.")] - public async Task TestPathRewriteMiddlewareForGraphQL( - string graphQLConfiguredPath, - string requestPath, - HttpStatusCode expectedStatusCode) - { - GraphQLRuntimeOptions graphqlOptions = new(Path: graphQLConfiguredPath); + /// + /// Tests that the custom path rewriting middleware properly rewrites the + /// first segment of a path (/segment1/.../segmentN) when the segment matches + /// the custom configured GraphQLEndpoint. + /// Note: The GraphQL service is always internally mapped to /graphql + /// + /// The custom configured GraphQL path in configuration + /// The path used in the web request executed in the test. + /// Expected Http success/error code + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/graphql", "/gql", HttpStatusCode.BadRequest, DisplayName = "Request to non-configured graphQL endpoint is handled by REST controller.")] + [DataRow("/graphql", "/graphql", HttpStatusCode.OK, DisplayName = "Request to configured default GraphQL endpoint succeeds, path not rewritten.")] + [DataRow("/gql", "/gql/additionalURLsegment", HttpStatusCode.OK, DisplayName = "GraphQL request path (with extra segments) rewritten to match internally set GraphQL endpoint /graphql.")] + [DataRow("/gql", "/gql", HttpStatusCode.OK, DisplayName = "GraphQL request path rewritten to match internally set GraphQL endpoint /graphql.")] + [DataRow("/gql", "/api/book", HttpStatusCode.NotFound, DisplayName = "Non-GraphQL request's path is not rewritten and is handled by REST controller.")] + [DataRow("/gql", "/graphql", HttpStatusCode.NotFound, DisplayName = "Requests to default/internally set graphQL endpoint fail when configured endpoint differs.")] + public async Task TestPathRewriteMiddlewareForGraphQL( + string graphQLConfiguredPath, + string requestPath, + HttpStatusCode expectedStatusCode) + { + GraphQLRuntimeOptions graphqlOptions = new(Path: graphQLConfiguredPath); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new()); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, new()); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" }; + string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - string query = @"{ + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + string query = @"{ book_by_pk(id: 1) { id, title, @@ -839,120 +839,120 @@ public async Task TestPathRewriteMiddlewareForGraphQL( } }"; - var payload = new { query }; + var payload = new { query }; - HttpRequestMessage request = new(HttpMethod.Post, requestPath) - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage request = new(HttpMethod.Post, requestPath) + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage response = await client.SendAsync(request); - string body = await response.Content.ReadAsStringAsync(); + HttpResponseMessage response = await client.SendAsync(request); + string body = await response.Content.ReadAsStringAsync(); - Assert.AreEqual(expectedStatusCode, response.StatusCode); - } + Assert.AreEqual(expectedStatusCode, response.StatusCode); + } - /// - /// Validates the error message that is returned for REST requests with incorrect parameter type - /// when the engine is running in Production mode. The error messages in Production mode is - /// very generic to not reveal information about the underlying database objects backing the entity. - /// This test runs against a MsSql database. However, generic error messages will be returned in Production - /// mode when run against PostgreSql and MySql databases. - /// - /// Type of REST request - /// Endpoint for the REST request - /// Right error message that should be shown to the end user - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(SupportedHttpVerb.Get, "/api/Book/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/books_view_all/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a view in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/GetBook?id=one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request on a stored-procedure with incorrect parameter type in production mode")] - [DataRow(SupportedHttpVerb.Get, "/api/GQLmappings/column1/one", null, "Invalid value provided for field: column1", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type with alias defined for primary key column on a table in production mode")] - [DataRow(SupportedHttpVerb.Post, "/api/Book", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a POST request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Put, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PUT request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Put, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a bad PUT request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PATCH request with incorrect primary key parameter type on a table in production mode")] - [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a PATCH request with incorrect parameter type in the request body on a table in production mode")] - [DataRow(SupportedHttpVerb.Delete, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a DELETE request with incorrect primary key parameter type on a table in production mode")] - public async Task TestGenericErrorMessageForRestApiInProductionMode( - SupportedHttpVerb requestType, - string requestPath, - string requestBody, - string expectedErrorMessage) - { - const string CUSTOM_CONFIG = "custom-config.json"; - TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL); - string[] args = new[] + /// + /// Validates the error message that is returned for REST requests with incorrect parameter type + /// when the engine is running in Production mode. The error messages in Production mode is + /// very generic to not reveal information about the underlying database objects backing the entity. + /// This test runs against a MsSql database. However, generic error messages will be returned in Production + /// mode when run against PostgreSql and MySql databases. + /// + /// Type of REST request + /// Endpoint for the REST request + /// Right error message that should be shown to the end user + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(SupportedHttpVerb.Get, "/api/Book/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/books_view_all/id/one", null, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type on a view in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/GetBook?id=one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a GET request on a stored-procedure with incorrect parameter type in production mode")] + [DataRow(SupportedHttpVerb.Get, "/api/GQLmappings/column1/one", null, "Invalid value provided for field: column1", DisplayName = "Validates the error message for a GET request with incorrect primary key parameter type with alias defined for primary key column on a table in production mode")] + [DataRow(SupportedHttpVerb.Post, "/api/Book", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a POST request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Put, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PUT request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Put, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a bad PUT request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a PATCH request with incorrect primary key parameter type on a table in production mode")] + [DataRow(SupportedHttpVerb.Patch, "/api/Book/id/1", REQUEST_BODY_WITH_INCORRECT_PARAM_TYPES, "Invalid value provided for field: publisher_id", DisplayName = "Validates the error message for a PATCH request with incorrect parameter type in the request body on a table in production mode")] + [DataRow(SupportedHttpVerb.Delete, "/api/Book/id/one", REQUEST_BODY_WITH_CORRECT_PARAM_TYPES, "Invalid value provided for field: id", DisplayName = "Validates the error message for a DELETE request with incorrect primary key parameter type on a table in production mode")] + public async Task TestGenericErrorMessageForRestApiInProductionMode( + SupportedHttpVerb requestType, + string requestPath, + string requestBody, + string expectedErrorMessage) { + const string CUSTOM_CONFIG = "custom-config.json"; + TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL); + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(requestType); - HttpRequestMessage request; - if (requestType is SupportedHttpVerb.Get || requestType is SupportedHttpVerb.Delete) + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) { - request = new(httpMethod, requestPath); - } - else - { - request = new(httpMethod, requestPath) + HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(requestType); + HttpRequestMessage request; + if (requestType is SupportedHttpVerb.Get || requestType is SupportedHttpVerb.Delete) { - Content = JsonContent.Create(requestBody) - }; - } + request = new(httpMethod, requestPath); + } + else + { + request = new(httpMethod, requestPath) + { + Content = JsonContent.Create(requestBody) + }; + } - HttpResponseMessage response = await client.SendAsync(request); - string body = await response.Content.ReadAsStringAsync(); - Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); - Assert.IsTrue(body.Contains(expectedErrorMessage)); + HttpResponseMessage response = await client.SendAsync(request); + string body = await response.Content.ReadAsStringAsync(); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); + Assert.IsTrue(body.Contains(expectedErrorMessage)); + } } - } - /// - /// Tests that the when Rest or GraphQL is disabled Globally, - /// any requests made will get a 404 response. - /// - /// The custom configured REST enabled property in configuration. - /// The custom configured GraphQL enabled property in configuration. - /// Expected HTTP status code code for the Rest request - /// Expected HTTP status code code for the GraphQL request - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] - public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( - bool isRestEnabled, - bool isGraphQLEnabled, - HttpStatusCode expectedStatusCodeForREST, - HttpStatusCode expectedStatusCodeForGraphQL, - string configurationEndpoint) - { - GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); - RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); + /// + /// Tests that the when Rest or GraphQL is disabled Globally, + /// any requests made will get a 404 response. + /// + /// The custom configured REST enabled property in configuration. + /// The custom configured GraphQL enabled property in configuration. + /// Expected HTTP status code code for the Rest request + /// Expected HTTP status code code for the GraphQL request + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] + [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] + [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] + [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] + [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] + [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] + public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( + bool isRestEnabled, + bool isGraphQLEnabled, + HttpStatusCode expectedStatusCodeForREST, + HttpStatusCode expectedStatusCodeForGraphQL, + string configurationEndpoint) + { + GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); + RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - // Non-Hosted Scenario - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - string query = @"{ + // Non-Hosted Scenario + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + string query = @"{ book_by_pk(id: 1) { id, title, @@ -960,78 +960,78 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir } }"; - object payload = new { query }; + object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); + HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); + Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); - HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); - } + HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); + HttpResponseMessage restResponse = await client.SendAsync(restRequest); + Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); + } - // Hosted Scenario - // Instantiate new server with no runtime config for post-startup configuration hydration tests. - using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) - using (HttpClient client = server.CreateClient()) - { - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + // Hosted Scenario + // Instantiate new server with no runtime config for post-startup configuration hydration tests. + using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) + using (HttpClient client = server.CreateClient()) + { + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpResponseMessage postResult = - await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + HttpResponseMessage postResult = + await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); + HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); - Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); + Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); - HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); + HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); - Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); + Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); + } } - } - /// - /// Engine supports config with some views that do not have keyfields specified in the config for MsSQL. - /// This Test validates that support. It creates a custom config with a view and no keyfields specified. - /// It checks both Rest and GraphQL queries are tested to return Success. - /// - [TestMethod, TestCategory(TestCategory.MSSQL)] - public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() - { - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - Entity viewEntity = new( - Source: new("books_view_all", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - GraphQL: new("", ""), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null - ); + /// + /// Engine supports config with some views that do not have keyfields specified in the config for MsSQL. + /// This Test validates that support. It creates a custom config with a view and no keyfields specified. + /// It checks both Rest and GraphQL queries are tested to return Success. + /// + [TestMethod, TestCategory(TestCategory.MSSQL)] + public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() + { + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + Entity viewEntity = new( + Source: new("books_view_all", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + GraphQL: new("", ""), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null + ); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), viewEntity, "books_view_all"); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, new(), new(), viewEntity, "books_view_all"); - const string CUSTOM_CONFIG = "custom-config.json"; + const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText( - CUSTOM_CONFIG, - configuration.ToJson()); + File.WriteAllText( + CUSTOM_CONFIG, + configuration.ToJson()); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - string query = @"{ + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + string query = @"{ books_view_alls { items{ id @@ -1040,465 +1040,465 @@ public async Task TestEngineSupportViewsWithoutKeyFieldsInConfigForMsSQL() } }"; - object payload = new { query }; + object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode); - string body = await graphQLResponse.Content.ReadAsStringAsync(); - Assert.IsFalse(body.Contains("errors")); // In GraphQL, All errors end up in the errors array, no matter what kind of error they are. + HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); + Assert.AreEqual(HttpStatusCode.OK, graphQLResponse.StatusCode); + string body = await graphQLResponse.Content.ReadAsStringAsync(); + Assert.IsFalse(body.Contains("errors")); // In GraphQL, All errors end up in the errors array, no matter what kind of error they are. - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/books_view_all"); - HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode); + HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/books_view_all"); + HttpResponseMessage restResponse = await client.SendAsync(restRequest); + Assert.AreEqual(HttpStatusCode.OK, restResponse.StatusCode); + } } - } - /// - /// Tests that Startup.cs properly handles EasyAuth authentication configuration. - /// AppService as Identity Provider while in Production mode will result in startup error. - /// An Azure AppService environment has environment variables on the host which indicate - /// the environment is, in fact, an AppService environment. - /// - /// HostMode in Runtime config - Development or Production. - /// EasyAuth auth type - AppService or StaticWebApps. - /// Whether to set the AppService host environment variables. - /// Whether an error is expected. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] - [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] - public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError) - { - // Clears or sets App Service Environment Variables based on test input. - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null); - Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null); - TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); - - FileSystem fileSystem = new(); - RuntimeConfigLoader loader = new(fileSystem); - - RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(loader); - RuntimeConfig config = configProvider.GetConfig(); - - // Setup configuration - AuthenticationOptions AuthenticationOptions = new(Provider: authType.ToString(), null); - RuntimeOptions runtimeOptions = new( - Rest: new(), - GraphQL: new(), - Host: new(null, AuthenticationOptions, hostMode) - ); - RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; - - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); - string[] args = new[] + /// + /// Tests that Startup.cs properly handles EasyAuth authentication configuration. + /// AppService as Identity Provider while in Production mode will result in startup error. + /// An Azure AppService environment has environment variables on the host which indicate + /// the environment is, in fact, an AppService environment. + /// + /// HostMode in Runtime config - Development or Production. + /// EasyAuth auth type - AppService or StaticWebApps. + /// Whether to set the AppService host environment variables. + /// Whether an error is expected. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] + [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")] + [DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")] + public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError) { + // Clears or sets App Service Environment Variables based on test input. + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null); + Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null); + TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); + + FileSystem fileSystem = new(); + RuntimeConfigLoader loader = new(fileSystem); + + RuntimeConfigProvider configProvider = TestHelper.GetRuntimeConfigProvider(loader); + RuntimeConfig config = configProvider.GetConfig(); + + // Setup configuration + AuthenticationOptions AuthenticationOptions = new(Provider: authType.ToString(), null); + RuntimeOptions runtimeOptions = new( + Rest: new(), + GraphQL: new(), + Host: new(null, AuthenticationOptions, hostMode) + ); + RuntimeConfig configWithCustomHostMode = config with { Runtime = runtimeOptions }; + + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configWithCustomHostMode.ToJson()); + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - // This test only checks for startup errors, so no requests are sent to the test server. - try - { - using TestServer server = new(Program.CreateWebHostBuilder(args)); - Assert.IsFalse(expectError, message: "Expected error faulting AppService config in production mode."); - } - catch (DataApiBuilderException ex) - { - Assert.IsTrue(expectError, message: ex.Message); - Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); + // This test only checks for startup errors, so no requests are sent to the test server. + try + { + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(expectError, message: "Expected error faulting AppService config in production mode."); + } + catch (DataApiBuilderException ex) + { + Assert.IsTrue(expectError, message: ex.Message); + Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message); + } } - } - /// - /// Integration test that validates schema introspection requests fail - /// when allow-introspection is false in the runtime configuration. - /// TestCategory is required for CI/CD pipeline to inject a connection string. - /// - /// - [TestCategory(TestCategory.MSSQL)] - [DataTestMethod] - [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT, DisplayName = "Disabled introspection returns GraphQL error.")] - [DataRow(true, false, null, CONFIGURATION_ENDPOINT, DisplayName = "Enabled introspection does not return introspection forbidden error.")] - [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT_V2, DisplayName = "Disabled introspection returns GraphQL error.")] - [DataRow(true, false, null, CONFIGURATION_ENDPOINT_V2, DisplayName = "Enabled introspection does not return introspection forbidden error.")] - public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool expectError, string errorMessage, string configurationEndpoint) - { - GraphQLRuntimeOptions graphqlOptions = new(AllowIntrospection: enableIntrospection); - RestRuntimeOptions restRuntimeOptions = new(); + /// + /// Integration test that validates schema introspection requests fail + /// when allow-introspection is false in the runtime configuration. + /// TestCategory is required for CI/CD pipeline to inject a connection string. + /// + /// + [TestCategory(TestCategory.MSSQL)] + [DataTestMethod] + [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT, DisplayName = "Disabled introspection returns GraphQL error.")] + [DataRow(true, false, null, CONFIGURATION_ENDPOINT, DisplayName = "Enabled introspection does not return introspection forbidden error.")] + [DataRow(false, true, "Introspection is not allowed for the current request.", CONFIGURATION_ENDPOINT_V2, DisplayName = "Disabled introspection returns GraphQL error.")] + [DataRow(true, false, null, CONFIGURATION_ENDPOINT_V2, DisplayName = "Enabled introspection does not return introspection forbidden error.")] + public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool expectError, string errorMessage, string configurationEndpoint) + { + GraphQLRuntimeOptions graphqlOptions = new(AllowIntrospection: enableIntrospection); + RestRuntimeOptions restRuntimeOptions = new(); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - await ExecuteGraphQLIntrospectionQueries(server, client, expectError); - } + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + await ExecuteGraphQLIntrospectionQueries(server, client, expectError); + } - // Instantiate new server with no runtime config for post-startup configuration hydration tests. - using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) - using (HttpClient client = server.CreateClient()) - { - JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); + // Instantiate new server with no runtime config for post-startup configuration hydration tests. + using (TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty()))) + using (HttpClient client = server.CreateClient()) + { + JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); - Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); + Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); - await ExecuteGraphQLIntrospectionQueries(server, client, expectError); + await ExecuteGraphQLIntrospectionQueries(server, client, expectError); + } } - } - /// - /// Indirectly tests IsGraphQLReservedName(). Runtime config provided to engine which will - /// trigger SqlMetadataProvider PopulateSourceDefinitionAsync() to pull column metadata from - /// the table "graphql_incompatible." That table contains columns which collide with reserved GraphQL - /// introspection field names which begin with double underscore (__). - /// - [TestCategory(TestCategory.MSSQL)] - [DataTestMethod] - [DataRow(true, true, "__typeName", "__introspectionField", true, DisplayName = "Name violation, fails since no proper mapping set.")] - [DataRow(true, true, "__typeName", "columnMapping", false, DisplayName = "Name violation, but OK since proper mapping set.")] - [DataRow(false, true, null, null, false, DisplayName = "Name violation, but OK since GraphQL globally disabled.")] - [DataRow(true, false, null, null, false, DisplayName = "Name violation, but OK since GraphQL disabled for entity.")] - public void TestInvalidDatabaseColumnNameHandling( - bool globalGraphQLEnabled, - bool entityGraphQLEnabled, - string columnName, - string columnMapping, - bool expectError) - { - GraphQLRuntimeOptions graphqlOptions = new(Enabled: globalGraphQLEnabled); - RestRuntimeOptions restRuntimeOptions = new(Enabled: true); + /// + /// Indirectly tests IsGraphQLReservedName(). Runtime config provided to engine which will + /// trigger SqlMetadataProvider PopulateSourceDefinitionAsync() to pull column metadata from + /// the table "graphql_incompatible." That table contains columns which collide with reserved GraphQL + /// introspection field names which begin with double underscore (__). + /// + [TestCategory(TestCategory.MSSQL)] + [DataTestMethod] + [DataRow(true, true, "__typeName", "__introspectionField", true, DisplayName = "Name violation, fails since no proper mapping set.")] + [DataRow(true, true, "__typeName", "columnMapping", false, DisplayName = "Name violation, but OK since proper mapping set.")] + [DataRow(false, true, null, null, false, DisplayName = "Name violation, but OK since GraphQL globally disabled.")] + [DataRow(true, false, null, null, false, DisplayName = "Name violation, but OK since GraphQL disabled for entity.")] + public void TestInvalidDatabaseColumnNameHandling( + bool globalGraphQLEnabled, + bool entityGraphQLEnabled, + string columnName, + string columnMapping, + bool expectError) + { + GraphQLRuntimeOptions graphqlOptions = new(Enabled: globalGraphQLEnabled); + RestRuntimeOptions restRuntimeOptions = new(Enabled: true); - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - // Configure Entity for testing - Dictionary mappings = new() + // Configure Entity for testing + Dictionary mappings = new() { { "__introspectionName", "conformingIntrospectionName" } }; - if (!string.IsNullOrWhiteSpace(columnMapping)) - { - mappings.Add(columnName, columnMapping); - } + if (!string.IsNullOrWhiteSpace(columnMapping)) + { + mappings.Add(columnName, columnMapping); + } - Entity entity = new( - Source: new("graphql_incompatible", EntitySourceType.Table, null, null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: mappings - ); + Entity entity = new( + Source: new("graphql_incompatible", EntitySourceType.Table, null, null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new("graphql_incompatible", "graphql_incompatibles", entityGraphQLEnabled), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: mappings + ); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, "graphqlNameCompat"); - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, entity, "graphqlNameCompat"); + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - try - { - using TestServer server = new(Program.CreateWebHostBuilder(args)); - Assert.IsFalse(expectError, message: "Expected startup to fail."); - } - catch (Exception ex) - { - Assert.IsTrue(expectError, message: "Startup was not expected to fail. " + ex.Message); + try + { + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsFalse(expectError, message: "Expected startup to fail."); + } + catch (Exception ex) + { + Assert.IsTrue(expectError, message: "Startup was not expected to fail. " + ex.Message); + } } - } - /// - /// Test different Swagger endpoints in different host modes when accessed interactively via browser. - /// Two pass request scheme: - /// 1 - Send get request to expected Swagger endpoint /swagger - /// Response - Internally Swagger sends HTTP 301 Moved Permanently with Location header - /// pointing to exact Swagger page (/swagger/index.html) - /// 2 - Send GET request to path referred to by Location header in previous response - /// Response - Successful loading of SwaggerUI HTML, with reference to endpoint used - /// to retrieve OpenAPI document. This test ensures that Swagger components load, but - /// does not confirm that a proper OpenAPI document was created. - /// - /// The custom REST route - /// The mode in which the service is executing. - /// Whether to expect an error. - /// Expected Status Code. - /// Snippet of expected HTML to be emitted from successful page load. - /// This should note the openapi route that Swagger will use to retrieve the OpenAPI document. - [DataTestMethod] - [TestCategory(TestCategory.MSSQL)] - [DataRow("/api", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/api/openapi\"", DisplayName = "SwaggerUI enabled in development mode.")] - [DataRow("/custompath", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/custompath/openapi\"", DisplayName = "SwaggerUI enabled with custom REST path in development mode.")] - [DataRow("/api", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode.")] - [DataRow("/custompath", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode with custom REST path.")] - public async Task OpenApi_InteractiveSwaggerUI( - string customRestPath, - HostMode hostModeType, - bool expectsError, - HttpStatusCode expectedStatusCode, - string expectedOpenApiTargetContent) - { - string swaggerEndpoint = "/swagger"; - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); - - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource: dataSource, new(), new(Path: customRestPath)); - configuration = configuration - with + /// + /// Test different Swagger endpoints in different host modes when accessed interactively via browser. + /// Two pass request scheme: + /// 1 - Send get request to expected Swagger endpoint /swagger + /// Response - Internally Swagger sends HTTP 301 Moved Permanently with Location header + /// pointing to exact Swagger page (/swagger/index.html) + /// 2 - Send GET request to path referred to by Location header in previous response + /// Response - Successful loading of SwaggerUI HTML, with reference to endpoint used + /// to retrieve OpenAPI document. This test ensures that Swagger components load, but + /// does not confirm that a proper OpenAPI document was created. + /// + /// The custom REST route + /// The mode in which the service is executing. + /// Whether to expect an error. + /// Expected Status Code. + /// Snippet of expected HTML to be emitted from successful page load. + /// This should note the openapi route that Swagger will use to retrieve the OpenAPI document. + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow("/api", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/api/openapi\"", DisplayName = "SwaggerUI enabled in development mode.")] + [DataRow("/custompath", HostMode.Development, false, HttpStatusCode.OK, "{\"urls\":[{\"url\":\"/custompath/openapi\"", DisplayName = "SwaggerUI enabled with custom REST path in development mode.")] + [DataRow("/api", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode.")] + [DataRow("/custompath", HostMode.Production, true, HttpStatusCode.BadRequest, "", DisplayName = "SwaggerUI disabled in production mode with custom REST path.")] + public async Task OpenApi_InteractiveSwaggerUI( + string customRestPath, + HostMode hostModeType, + bool expectsError, + HttpStatusCode expectedStatusCode, + string expectedOpenApiTargetContent) { - Runtime = configuration.Runtime - with + string swaggerEndpoint = "/swagger"; + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource: dataSource, new(), new(Path: customRestPath)); + configuration = configuration + with { - Host = configuration.Runtime.Host - with - { Mode = hostModeType } - } - }; - const string CUSTOM_CONFIG = "custom-config.json"; - File.WriteAllText( - CUSTOM_CONFIG, - configuration.ToJson()); + Runtime = configuration.Runtime + with + { + Host = configuration.Runtime.Host + with + { Mode = hostModeType } + } + }; + const string CUSTOM_CONFIG = "custom-config.json"; + File.WriteAllText( + CUSTOM_CONFIG, + configuration.ToJson()); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG}" }; - using (TestServer server = new(Program.CreateWebHostBuilder(args))) - using (HttpClient client = server.CreateClient()) - { - HttpRequestMessage initialRequest = new(HttpMethod.Get, swaggerEndpoint); + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + HttpRequestMessage initialRequest = new(HttpMethod.Get, swaggerEndpoint); - // Adding the following headers simulates an interactive browser request. - initialRequest.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); - initialRequest.Headers.Add("accept", BROWSER_ACCEPT_HEADER); + // Adding the following headers simulates an interactive browser request. + initialRequest.Headers.Add("user-agent", BROWSER_USER_AGENT_HEADER); + initialRequest.Headers.Add("accept", BROWSER_ACCEPT_HEADER); - HttpResponseMessage response = await client.SendAsync(initialRequest); - if (expectsError) - { - // Redirect(HTTP 301) and follow up request to the returned path - // do not occur in a failure scenario. Only HTTP 400 (Bad Request) - // is expected. - Assert.AreEqual(expectedStatusCode, response.StatusCode); - } - else - { - // Swagger endpoint internally configured to reroute from /swagger to /swagger/index.html - Assert.AreEqual(HttpStatusCode.MovedPermanently, response.StatusCode); + HttpResponseMessage response = await client.SendAsync(initialRequest); + if (expectsError) + { + // Redirect(HTTP 301) and follow up request to the returned path + // do not occur in a failure scenario. Only HTTP 400 (Bad Request) + // is expected. + Assert.AreEqual(expectedStatusCode, response.StatusCode); + } + else + { + // Swagger endpoint internally configured to reroute from /swagger to /swagger/index.html + Assert.AreEqual(HttpStatusCode.MovedPermanently, response.StatusCode); - HttpRequestMessage followUpRequest = new(HttpMethod.Get, response.Headers.Location); - HttpResponseMessage followUpResponse = await client.SendAsync(followUpRequest); - Assert.AreEqual(expectedStatusCode, followUpResponse.StatusCode); + HttpRequestMessage followUpRequest = new(HttpMethod.Get, response.Headers.Location); + HttpResponseMessage followUpResponse = await client.SendAsync(followUpRequest); + Assert.AreEqual(expectedStatusCode, followUpResponse.StatusCode); - // Validate that Swagger requests OpenAPI document using REST path defined in runtime config. - string actualBody = await followUpResponse.Content.ReadAsStringAsync(); - Assert.AreEqual(true, actualBody.Contains(expectedOpenApiTargetContent)); + // Validate that Swagger requests OpenAPI document using REST path defined in runtime config. + string actualBody = await followUpResponse.Content.ReadAsStringAsync(); + Assert.AreEqual(true, actualBody.Contains(expectedOpenApiTargetContent)); + } } } - } - /// - /// Validates the OpenAPI documentor behavior when enabling and disabling the global REST endpoint - /// for the DAB engine. - /// Global REST enabled: - /// - GET to /openapi returns the created OpenAPI document and succeeds with 200 OK. - /// Global REST disabled: - /// - GET to /openapi fails with 404 Not Found. - /// - [DataTestMethod] - [DataRow(true, false, DisplayName = "Global REST endpoint enabled - successful OpenAPI doc retrieval")] - [DataRow(false, true, DisplayName = "Global REST endpoint disabled - OpenAPI doc does not exist - HTTP404 NotFound.")] - [TestCategory(TestCategory.MSSQL)] - public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expectsError) - { - // At least one entity is required in the runtime config for the engine to start. - // Even though this entity is not under test, it must be supplied to the config - // file creation function. - Entity requiredEntity = new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: new(Array.Empty(), Enabled: false), - GraphQL: new("book", "books"), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Dictionary entityMap = new() + /// + /// Validates the OpenAPI documentor behavior when enabling and disabling the global REST endpoint + /// for the DAB engine. + /// Global REST enabled: + /// - GET to /openapi returns the created OpenAPI document and succeeds with 200 OK. + /// Global REST disabled: + /// - GET to /openapi fails with 404 Not Found. + /// + [DataTestMethod] + [DataRow(true, false, DisplayName = "Global REST endpoint enabled - successful OpenAPI doc retrieval")] + [DataRow(false, true, DisplayName = "Global REST endpoint disabled - OpenAPI doc does not exist - HTTP404 NotFound.")] + [TestCategory(TestCategory.MSSQL)] + public async Task OpenApi_GlobalEntityRestPath(bool globalRestEnabled, bool expectsError) + { + // At least one entity is required in the runtime config for the engine to start. + // Even though this entity is not under test, it must be supplied to the config + // file creation function. + Entity requiredEntity = new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: new(Array.Empty(), Enabled: false), + GraphQL: new("book", "books"), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() { { "Book", requiredEntity } }; - CreateCustomConfigFile(globalRestEnabled: globalRestEnabled, entityMap); + CreateCustomConfigFile(globalRestEnabled: globalRestEnabled, entityMap); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); - HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + // Setup and send GET request + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{OPENAPI_DOCUMENT_ENDPOINT}"); + HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); - // Validate response - if (expectsError) - { - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); - } - else - { - // Process response body - string responseBody = await response.Content.ReadAsStringAsync(); - Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); + // Validate response + if (expectsError) + { + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } + else + { + // Process response body + string responseBody = await response.Content.ReadAsStringAsync(); + Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); - // Validate response body - Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); - ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); + // Validate response body + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); + } } - } - /// - /// Validates the behavior of the OpenApiDocumentor when the runtime config has entities with - /// REST endpoint enabled and disabled. - /// Enabled -> path should be created - /// Disabled -> path not created and is excluded from OpenApi document. - /// - [TestCategory(TestCategory.MSSQL)] - [TestMethod] - public async Task OpenApi_EntityLevelRestEndpoint() - { - // Create the entities under test. - Entity restEnabledEntity = new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), - GraphQL: new("", "", false), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Entity restDisabledEntity = new( - Source: new("publishers", EntitySourceType.Table, null, null), - Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false), - GraphQL: new("publisher", "publishers", true), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null); - - Dictionary entityMap = new() + /// + /// Validates the behavior of the OpenApiDocumentor when the runtime config has entities with + /// REST endpoint enabled and disabled. + /// Enabled -> path should be created + /// Disabled -> path not created and is excluded from OpenApi document. + /// + [TestCategory(TestCategory.MSSQL)] + [TestMethod] + public async Task OpenApi_EntityLevelRestEndpoint() + { + // Create the entities under test. + Entity restEnabledEntity = new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS), + GraphQL: new("", "", false), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Entity restDisabledEntity = new( + Source: new("publishers", EntitySourceType.Table, null, null), + Rest: new(EntityRestOptions.DEFAULT_SUPPORTED_VERBS, Enabled: false), + GraphQL: new("publisher", "publishers", true), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() { { "Book", restEnabledEntity }, { "Publisher", restDisabledEntity } }; - CreateCustomConfigFile(globalRestEnabled: true, entityMap); + CreateCustomConfigFile(globalRestEnabled: true, entityMap); - string[] args = new[] - { + string[] args = new[] + { $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" }; - using TestServer server = new(Program.CreateWebHostBuilder(args)); - using HttpClient client = server.CreateClient(); - // Setup and send GET request - HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Core.Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); - HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); - - // Parse response metadata - string responseBody = await response.Content.ReadAsStringAsync(); - Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); - - // Validate response metadata - ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); - JsonElement pathsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS]; - - // Validate that paths were created for the entity with REST enabled. - Assert.IsTrue(pathsElement.TryGetProperty("/Book", out _)); - Assert.IsTrue(pathsElement.TryGetProperty("/Book/id/{id}", out _)); - - // Validate that paths were not created for the entity with REST disabled. - Assert.IsFalse(pathsElement.TryGetProperty("/Publisher", out _)); - Assert.IsFalse(pathsElement.TryGetProperty("/Publisher/id/{id}", out _)); - - JsonElement componentsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS]; - Assert.IsTrue(componentsElement.TryGetProperty(OpenApiDocumentorConstants.PROPERTY_SCHEMAS, out JsonElement componentSchemasElement)); - // Validate that components were created for the entity with REST enabled. - Assert.IsTrue(componentSchemasElement.TryGetProperty("Book_NoPK", out _)); - Assert.IsTrue(componentSchemasElement.TryGetProperty("Book", out _)); - - // Validate that components were not created for the entity with REST disabled. - Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher_NoPK", out _)); - Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher", out _)); - } + using TestServer server = new(Program.CreateWebHostBuilder(args)); + using HttpClient client = server.CreateClient(); + // Setup and send GET request + HttpRequestMessage readOpenApiDocumentRequest = new(HttpMethod.Get, $"{RestRuntimeOptions.DEFAULT_PATH}/{Core.Services.OpenAPI.OpenApiDocumentor.OPENAPI_ROUTE}"); + HttpResponseMessage response = await client.SendAsync(readOpenApiDocumentRequest); - /// - /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings - /// using the supplied entities. - /// - /// flag to enable or disabled REST globally. - /// Collection of entityName -> Entity object. - private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary entityMap) - { - DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + // Parse response metadata + string responseBody = await response.Content.ReadAsStringAsync(); + Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); - RuntimeConfig runtimeConfig = new( - Schema: string.Empty, - DataSource: dataSource, - Runtime: new( - Rest: new(Enabled: globalRestEnabled), - GraphQL: new(), - Host: new(null, null) - ), - Entities: new(entityMap)); + // Validate response metadata + ValidateOpenApiDocTopLevelPropertiesExist(responseProperties); + JsonElement pathsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS]; - File.WriteAllText( - path: CUSTOM_CONFIG_FILENAME, - contents: runtimeConfig.ToJson()); - } + // Validate that paths were created for the entity with REST enabled. + Assert.IsTrue(pathsElement.TryGetProperty("/Book", out _)); + Assert.IsTrue(pathsElement.TryGetProperty("/Book/id/{id}", out _)); - /// - /// Validates that all the OpenAPI description document's top level properties exist. - /// A failure here indicates that there was an undetected failure creating the OpenAPI document. - /// - /// Represent a deserialized JSON result from retrieving the OpenAPI document - private static void ValidateOpenApiDocTopLevelPropertiesExist(Dictionary responseProperties) - { - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_OPENAPI)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_INFO)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_SERVERS)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS)); - Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS)); - } + // Validate that paths were not created for the entity with REST disabled. + Assert.IsFalse(pathsElement.TryGetProperty("/Publisher", out _)); + Assert.IsFalse(pathsElement.TryGetProperty("/Publisher/id/{id}", out _)); - /// - /// Validates that schema introspection requests fail when allow-introspection is false in the runtime configuration. - /// - /// - private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, HttpClient client, bool expectError) - { - string graphQLQueryName = "__schema"; - string graphQLQuery = @"{ + JsonElement componentsElement = responseProperties[OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS]; + Assert.IsTrue(componentsElement.TryGetProperty(OpenApiDocumentorConstants.PROPERTY_SCHEMAS, out JsonElement componentSchemasElement)); + // Validate that components were created for the entity with REST enabled. + Assert.IsTrue(componentSchemasElement.TryGetProperty("Book_NoPK", out _)); + Assert.IsTrue(componentSchemasElement.TryGetProperty("Book", out _)); + + // Validate that components were not created for the entity with REST disabled. + Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher_NoPK", out _)); + Assert.IsFalse(componentSchemasElement.TryGetProperty("Publisher", out _)); + } + + /// + /// Helper function to write custom configuration file. with minimal REST/GraphQL global settings + /// using the supplied entities. + /// + /// flag to enable or disabled REST globally. + /// Collection of entityName -> Entity object. + private static void CreateCustomConfigFile(bool globalRestEnabled, Dictionary entityMap) + { + DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), new()); + + RuntimeConfig runtimeConfig = new( + Schema: string.Empty, + DataSource: dataSource, + Runtime: new( + Rest: new(Enabled: globalRestEnabled), + GraphQL: new(), + Host: new(null, null) + ), + Entities: new(entityMap)); + + File.WriteAllText( + path: CUSTOM_CONFIG_FILENAME, + contents: runtimeConfig.ToJson()); + } + + /// + /// Validates that all the OpenAPI description document's top level properties exist. + /// A failure here indicates that there was an undetected failure creating the OpenAPI document. + /// + /// Represent a deserialized JSON result from retrieving the OpenAPI document + private static void ValidateOpenApiDocTopLevelPropertiesExist(Dictionary responseProperties) + { + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_OPENAPI)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_INFO)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_SERVERS)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_PATHS)); + Assert.IsTrue(responseProperties.ContainsKey(OpenApiDocumentorConstants.TOPLEVELPROPERTY_COMPONENTS)); + } + + /// + /// Validates that schema introspection requests fail when allow-introspection is false in the runtime configuration. + /// + /// + private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, HttpClient client, bool expectError) + { + string graphQLQueryName = "__schema"; + string graphQLQuery = @"{ __schema { types { name @@ -1506,239 +1506,239 @@ private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server, } }"; - string expectedErrorMessageFragment = "Introspection is not allowed for the current request."; - - try - { - RuntimeConfigProvider configProvider = server.Services.GetRequiredService(); - - JsonElement actual = await GraphQLRequestExecutor.PostGraphQLRequestAsync( - client, - configProvider, - query: graphQLQuery, - queryName: graphQLQueryName, - variables: null, - clientRoleHeader: null - ); + string expectedErrorMessageFragment = "Introspection is not allowed for the current request."; - if (expectError) + try { - SqlTestHelper.TestForErrorInGraphQLResponse( - response: actual.ToString(), - message: expectedErrorMessageFragment, - statusCode: ErrorCodes.Validation.IntrospectionNotAllowed - ); + RuntimeConfigProvider configProvider = server.Services.GetRequiredService(); + + JsonElement actual = await GraphQLRequestExecutor.PostGraphQLRequestAsync( + client, + configProvider, + query: graphQLQuery, + queryName: graphQLQueryName, + variables: null, + clientRoleHeader: null + ); + + if (expectError) + { + SqlTestHelper.TestForErrorInGraphQLResponse( + response: actual.ToString(), + message: expectedErrorMessageFragment, + statusCode: ErrorCodes.Validation.IntrospectionNotAllowed + ); + } + } + catch (Exception ex) + { + // ExecuteGraphQLRequestAsync will raise an exception when no "data" key + // exists in the GraphQL JSON response. + Assert.Fail(message: "No schema metadata in GraphQL response." + ex.Message); } } - catch (Exception ex) - { - // ExecuteGraphQLRequestAsync will raise an exception when no "data" key - // exists in the GraphQL JSON response. - Assert.Fail(message: "No schema metadata in GraphQL response." + ex.Message); - } - } - private static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false) - { - if (CONFIGURATION_ENDPOINT == endpoint) + private static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false) { - ConfigurationPostParameters configParams = GetCosmosConfigurationParameters(); - if (config != null) + if (CONFIGURATION_ENDPOINT == endpoint) { - configParams = configParams with { Configuration = config }; - } + ConfigurationPostParameters configParams = GetCosmosConfigurationParameters(); + if (config != null) + { + configParams = configParams with { Configuration = config }; + } - if (useAccessToken) - { - configParams = configParams with + if (useAccessToken) { - ConnectionString = "AccountEndpoint=https://localhost:8081/;", - AccessToken = GenerateMockJwtToken() - }; - } + configParams = configParams with + { + ConnectionString = "AccountEndpoint=https://localhost:8081/;", + AccessToken = GenerateMockJwtToken() + }; + } - return JsonContent.Create(configParams); - } - else if (CONFIGURATION_ENDPOINT_V2 == endpoint) - { - ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2(); - if (config != null) - { - configParams = configParams with { Configuration = config }; + return JsonContent.Create(configParams); } - - if (useAccessToken) + else if (CONFIGURATION_ENDPOINT_V2 == endpoint) { - // With an invalid access token, when a new instance of CosmosClient is created with that token, it - // won't throw an exception. But when a graphql request is coming in, that's when it throws a 401 - // exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property - // from the token, if it's not valid, then we will throw an exception from our code before it - // initiating a client. Uses a valid fake JWT access token for testing purposes. - RuntimeConfig overrides = new(null, new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()), null, null); - - configParams = configParams with + ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2(); + if (config != null) { - ConfigurationOverrides = overrides.ToJson(), - AccessToken = GenerateMockJwtToken() - }; - } + configParams = configParams with { Configuration = config }; + } - return JsonContent.Create(configParams); - } - else - { - throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}"); - } - } + if (useAccessToken) + { + // With an invalid access token, when a new instance of CosmosClient is created with that token, it + // won't throw an exception. But when a graphql request is coming in, that's when it throws a 401 + // exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property + // from the token, if it's not valid, then we will throw an exception from our code before it + // initiating a client. Uses a valid fake JWT access token for testing purposes. + RuntimeConfig overrides = new(null, new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()), null, null); + + configParams = configParams with + { + ConfigurationOverrides = overrides.ToJson(), + AccessToken = GenerateMockJwtToken() + }; + } - private static string GenerateMockJwtToken() - { - string mySecret = "PlaceholderPlaceholder"; - SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret)); + return JsonContent.Create(configParams); + } + else + { + throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}"); + } + } - JwtSecurityTokenHandler tokenHandler = new(); - SecurityTokenDescriptor tokenDescriptor = new() + private static string GenerateMockJwtToken() { - Subject = new ClaimsIdentity(new Claim[] { }), - Expires = DateTime.UtcNow.AddMinutes(5), - Issuer = "http://mysite.com", - Audience = "http://myaudience.com", - SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature) - }; - - SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); - } - - private static ConfigurationPostParameters GetCosmosConfigurationParameters() - { - string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; - return new( - File.ReadAllText(cosmosFile), - File.ReadAllText("schema.gql"), - $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", - AccessToken: null); - } + string mySecret = "PlaceholderPlaceholder"; + SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret)); - private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2() - { - string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; - RuntimeConfig overrides = new( - null, - new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()), - null, - null); - - return new( - File.ReadAllText(cosmosFile), - overrides.ToJson(), - File.ReadAllText("schema.gql"), - AccessToken: null); - } - - /// - /// Helper used to create the post-startup configuration payload sent to configuration controller. - /// Adds entity used to hydrate authorization resolver post-startup and validate that hydration succeeds. - /// Additional pre-processing performed acquire database connection string from a local file. - /// - /// ConfigurationPostParameters object. - private static JsonContent GetPostStartupConfigParams(string environment, RuntimeConfig runtimeConfig, string configurationEndpoint) - { - string connectionString = GetConnectionStringFromEnvironmentConfig(environment); + JwtSecurityTokenHandler tokenHandler = new(); + SecurityTokenDescriptor tokenDescriptor = new() + { + Subject = new ClaimsIdentity(new Claim[] { }), + Expires = DateTime.UtcNow.AddMinutes(5), + Issuer = "http://mysite.com", + Audience = "http://myaudience.com", + SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature) + }; - string serializedConfiguration = runtimeConfig.ToJson(); + SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } - if (configurationEndpoint == CONFIGURATION_ENDPOINT) + private static ConfigurationPostParameters GetCosmosConfigurationParameters() { - ConfigurationPostParameters returnParams = new( - Configuration: serializedConfiguration, - Schema: null, - ConnectionString: connectionString, + string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; + return new( + File.ReadAllText(cosmosFile), + File.ReadAllText("schema.gql"), + $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", AccessToken: null); - return JsonContent.Create(returnParams); } - else if (configurationEndpoint == CONFIGURATION_ENDPOINT_V2) - { - RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()), null, null); - ConfigurationPostParametersV2 returnParams = new( - Configuration: serializedConfiguration, - ConfigurationOverrides: overrides.ToJson(), - Schema: null, + private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2() + { + string cosmosFile = $"{RuntimeConfigLoader.CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{RuntimeConfigLoader.CONFIG_EXTENSION}"; + RuntimeConfig overrides = new( + null, + new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()), + null, + null); + + return new( + File.ReadAllText(cosmosFile), + overrides.ToJson(), + File.ReadAllText("schema.gql"), AccessToken: null); - - return JsonContent.Create(returnParams); } - else + + /// + /// Helper used to create the post-startup configuration payload sent to configuration controller. + /// Adds entity used to hydrate authorization resolver post-startup and validate that hydration succeeds. + /// Additional pre-processing performed acquire database connection string from a local file. + /// + /// ConfigurationPostParameters object. + private static JsonContent GetPostStartupConfigParams(string environment, RuntimeConfig runtimeConfig, string configurationEndpoint) { - throw new InvalidOperationException("Invalid configurationEndpoint"); - } - } + string connectionString = GetConnectionStringFromEnvironmentConfig(environment); - /// - /// Hydrates configuration after engine has started and triggers service instantiation - /// by executing HTTP requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// Post-startup configuration - /// ServiceUnavailable if service is not successfully hydrated with config - private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) - { - // Hydrate configuration post-startup - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + string serializedConfiguration = runtimeConfig.ToJson(); - return await GetRestResponsePostConfigHydration(httpClient); - } + if (configurationEndpoint == CONFIGURATION_ENDPOINT) + { + ConfigurationPostParameters returnParams = new( + Configuration: serializedConfiguration, + Schema: null, + ConnectionString: connectionString, + AccessToken: null); + return JsonContent.Create(returnParams); + } + else if (configurationEndpoint == CONFIGURATION_ENDPOINT_V2) + { + RuntimeConfig overrides = new(null, new DataSource(DatabaseType.MSSQL, connectionString, new()), null, null); - /// - /// Executing REST requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// ServiceUnavailable if service is not successfully hydrated with config, - /// else the response code from the REST request - private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) - { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; - HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) - { - // Spot test authorization resolver utilization to ensure configuration is used. - HttpResponseMessage postConfigHydrationResult = - await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); - responseCode = postConfigHydrationResult.StatusCode; + ConfigurationPostParametersV2 returnParams = new( + Configuration: serializedConfiguration, + ConfigurationOverrides: overrides.ToJson(), + Schema: null, + AccessToken: null); - if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) + return JsonContent.Create(returnParams); + } + else { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); - continue; + throw new InvalidOperationException("Invalid configurationEndpoint"); } + } + + /// + /// Hydrates configuration after engine has started and triggers service instantiation + /// by executing HTTP requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// Post-startup configuration + /// ServiceUnavailable if service is not successfully hydrated with config + private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) + { + // Hydrate configuration post-startup + HttpResponseMessage postResult = + await httpClient.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - break; + return await GetRestResponsePostConfigHydration(httpClient); } - return responseCode; - } + /// + /// Executing REST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the REST request + private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) + { + // Retry request RETRY_COUNT times in 1 second increments to allow required services + // time to instantiate and hydrate permissions. + int retryCount = RETRY_COUNT; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount > 0) + { + // Spot test authorization resolver utilization to ensure configuration is used. + HttpResponseMessage postConfigHydrationResult = + await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); + responseCode = postConfigHydrationResult.StatusCode; - /// - /// Executing GraphQL POST requests against the engine until a non-503 error is received. - /// - /// Client used for request execution. - /// ServiceUnavailable if service is not successfully hydrated with config, - /// else the response code from the GRAPHQL request - private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) - { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; - HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) + if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) + { + retryCount--; + Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + continue; + } + + break; + } + + return responseCode; + } + + /// + /// Executing GraphQL POST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the GRAPHQL request + private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) { - string query = @"{ + // Retry request RETRY_COUNT times in 1 second increments to allow required services + // time to instantiate and hydrate permissions. + int retryCount = RETRY_COUNT; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount > 0) + { + string query = @"{ book_by_pk(id: 1) { id, title, @@ -1746,129 +1746,130 @@ private static async Task GetGraphQLResponsePostConfigHydration( } }"; - object payload = new { query }; + object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") - { - Content = JsonContent.Create(payload) - }; + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + { + Content = JsonContent.Create(payload) + }; - HttpResponseMessage graphQLResponse = await httpClient.SendAsync(graphQLRequest); - responseCode = graphQLResponse.StatusCode; + HttpResponseMessage graphQLResponse = await httpClient.SendAsync(graphQLRequest); + responseCode = graphQLResponse.StatusCode; - if (responseCode == HttpStatusCode.ServiceUnavailable) - { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); - continue; + if (responseCode == HttpStatusCode.ServiceUnavailable) + { + retryCount--; + Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + continue; + } + + break; } - break; + return responseCode; } - return responseCode; - } - - /// - /// Instantiate minimal runtime config with custom global settings. - /// - /// DataSource to pull connection string required for engine start. - /// - public static RuntimeConfig InitMinimalRuntimeConfig( - DataSource dataSource, - GraphQLRuntimeOptions graphqlOptions, - RestRuntimeOptions restOptions, - Entity entity = null, - string entityName = null) - { - entity ??= new( - Source: new("books", EntitySourceType.Table, null, null), - Rest: null, - GraphQL: new(Singular: "book", Plural: "books"), - Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, - Relationships: null, - Mappings: null - ); + /// + /// Instantiate minimal runtime config with custom global settings. + /// + /// DataSource to pull connection string required for engine start. + /// + public static RuntimeConfig InitMinimalRuntimeConfig( + DataSource dataSource, + GraphQLRuntimeOptions graphqlOptions, + RestRuntimeOptions restOptions, + Entity entity = null, + string entityName = null) + { + entity ??= new( + Source: new("books", EntitySourceType.Table, null, null), + Rest: null, + GraphQL: new(Singular: "book", Plural: "books"), + Permissions: new[] { GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null + ); - entityName ??= "Book"; + entityName ??= "Book"; - Dictionary entityMap = new() + Dictionary entityMap = new() { { entityName, entity } }; - return new( - Schema: "IntegrationTestMinimalSchema", - DataSource: dataSource, - Runtime: new(restOptions, graphqlOptions, new(null, null)), - Entities: new(entityMap) - ); - } + return new( + Schema: "IntegrationTestMinimalSchema", + DataSource: dataSource, + Runtime: new(restOptions, graphqlOptions, new(null, null)), + Entities: new(entityMap) + ); + } - /// - /// Gets PermissionSetting object allowed to perform all actions. - /// - /// Name of role to assign to permission - /// PermissionSetting - public static EntityPermission GetMinimalPermissionConfig(string roleName) - { - EntityAction actionForRole = new( - Action: EntityActionOperation.All, - Fields: null, - Policy: new() - ); - - return new EntityPermission( - Role: roleName, - Actions: new[] { actionForRole } - ); - } + /// + /// Gets PermissionSetting object allowed to perform all actions. + /// + /// Name of role to assign to permission + /// PermissionSetting + public static EntityPermission GetMinimalPermissionConfig(string roleName) + { + EntityAction actionForRole = new( + Action: EntityActionOperation.All, + Fields: null, + Policy: new() + ); - /// - /// Reads configuration file for defined environment to acquire the connection string. - /// CI/CD Pipelines and local environments may not have connection string set as environment variable. - /// - /// Environment such as TestCategory.MSSQL - /// Connection string - public static string GetConnectionStringFromEnvironmentConfig(string environment) - { - FileSystem fileSystem = new(); - string sqlFile = new RuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); - string configPayload = File.ReadAllText(sqlFile); + return new EntityPermission( + Role: roleName, + Actions: new[] { actionForRole } + ); + } - RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig); + /// + /// Reads configuration file for defined environment to acquire the connection string. + /// CI/CD Pipelines and local environments may not have connection string set as environment variable. + /// + /// Environment such as TestCategory.MSSQL + /// Connection string + public static string GetConnectionStringFromEnvironmentConfig(string environment) + { + FileSystem fileSystem = new(); + string sqlFile = new RuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); + string configPayload = File.ReadAllText(sqlFile); - return runtimeConfig.DataSource.ConnectionString; - } + RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig); - private static void ValidateCosmosDbSetup(TestServer server) - { - object metadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); - Assert.IsInstanceOfType(metadataProvider, typeof(CosmosSqlMetadataProvider)); + return runtimeConfig.DataSource.ConnectionString; + } - object queryEngine = server.Services.GetService(typeof(IQueryEngine)); - Assert.IsInstanceOfType(queryEngine, typeof(CosmosQueryEngine)); + private static void ValidateCosmosDbSetup(TestServer server) + { + object metadataProvider = server.Services.GetService(typeof(ISqlMetadataProvider)); + Assert.IsInstanceOfType(metadataProvider, typeof(CosmosSqlMetadataProvider)); - object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); - Assert.IsInstanceOfType(mutationEngine, typeof(CosmosMutationEngine)); + object queryEngine = server.Services.GetService(typeof(IQueryEngine)); + Assert.IsInstanceOfType(queryEngine, typeof(CosmosQueryEngine)); - CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; - Assert.IsNotNull(cosmosClientProvider); - Assert.IsNotNull(cosmosClientProvider.Client); - } + object mutationEngine = server.Services.GetService(typeof(IMutationEngine)); + Assert.IsInstanceOfType(mutationEngine, typeof(CosmosMutationEngine)); - private bool HandleException(Exception e) where T : Exception - { - if (e is AggregateException aggregateException) - { - aggregateException.Handle(HandleException); - return true; + CosmosClientProvider cosmosClientProvider = server.Services.GetService(typeof(CosmosClientProvider)) as CosmosClientProvider; + Assert.IsNotNull(cosmosClientProvider); + Assert.IsNotNull(cosmosClientProvider.Client); } - else if (e is T) + + private bool HandleException(Exception e) where T : Exception { - return true; - } + if (e is AggregateException aggregateException) + { + aggregateException.Handle(HandleException); + return true; + } + else if (e is T) + { + return true; + } - return false; + return false; + } } } diff --git a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs index 4b23b98117..c3ce2052e8 100644 --- a/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs +++ b/src/Service.Tests/OpenApiDocumentor/CLRtoJsonValueTypeUnitTests.cs @@ -34,6 +34,7 @@ public class CLRtoJsonValueTypeUnitTests /// Raw string provided by database e.g. 'bigint' /// Whether DAB supports the resolved SqlDbType value. [TestMethod] + [DataRow("UnsupportedTypeName", false, DisplayName = "Validate unexpected SqlDbType name value is handled gracefully.")] [DynamicData(nameof(GetTestData_SupportedSystemTypesMapToJsonValueType), DynamicDataSourceType.Method)] public void SupportedSystemTypesMapToJsonValueType(string sqlDataTypeLiteral, bool isSupportedSqlDataType) {