diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/DatabaseInteractor.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/DatabaseInteractor.cs new file mode 100644 index 0000000000..2f96c6c8fb --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/DatabaseInteractor.cs @@ -0,0 +1,45 @@ +using Cosmos.GraphQL.Service.Resolvers; + +namespace Cosmos.GraphQL.Service.Tests.MsSql +{ + /// + /// Class that provides functions to interact with a database. + /// + public class DatabaseInteractor + { + public IQueryExecutor QueryExecutor { get; private set; } + + public DatabaseInteractor(IQueryExecutor queryExecutor) + { + QueryExecutor = queryExecutor; + } + + /// + /// Inserts data into the database. + /// + public void InsertData(string tableName, string values) + { + _ = QueryExecutor.ExecuteQueryAsync($"INSERT INTO {tableName} VALUES({values});", null).Result; + } + + /// + /// Creates a table in the database with provided name and columns + /// + public void CreateTable(string tableName, string columns) + { + _ = QueryExecutor.ExecuteQueryAsync($"CREATE TABLE {tableName} ({columns});", null).Result; + } + + /// + /// Drops all tables in the database + /// + public void DropTable(string tableName) + { + // Drops all tables in the database. + string dropTable = string.Format( + @"DROP TABLE IF EXISTS {0};", tableName); + + _ = QueryExecutor.ExecuteQueryAsync(sqltext: dropTable, parameters: null).Result; + } + } +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlQueryTests.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlQueryTests.cs new file mode 100644 index 0000000000..7de49404e6 --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlQueryTests.cs @@ -0,0 +1,180 @@ +using System.Data.Common; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Cosmos.GraphQL.Service.Controllers; +using Cosmos.GraphQL.Service.Resolvers; +using Cosmos.GraphQL.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.Data.SqlClient; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Cosmos.GraphQL.Service.Tests.MsSql +{ + /// + /// Test GraphQL Queries validating proper resolver/engine operation. + /// + [TestClass, TestCategory(TestCategory.MsSql)] + public class MsSqlQueryTests + { + #region Test Fixture Setup + private static IMetadataStoreProvider _metadataStoreProvider; + private static IQueryExecutor _queryExecutor; + private static IQueryBuilder _queryBuilder; + private static IQueryEngine _queryEngine; + private static GraphQLService _graphQLService; + private static GraphQLController _graphQLController; + private static DatabaseInteractor _databaseInteractor; + + public static string IntegrationTableName { get; } = "character"; + + /// + /// Sets up test fixture for class, only to be run once per test run, as defined by + /// MSTest decorator. + /// + /// + [ClassInitialize] + public static void InitializeTestFixure(TestContext context) + { + // Setup Schema and Resolvers + // + _metadataStoreProvider = new MetadataStoreProviderForTest(); + _metadataStoreProvider.StoreGraphQLSchema(MsSqlTestHelper.GraphQLSchema); + _metadataStoreProvider.StoreQueryResolver(MsSqlTestHelper.GetQueryResolverJson(MsSqlTestHelper.CharacterByIdResolver)); + _metadataStoreProvider.StoreQueryResolver(MsSqlTestHelper.GetQueryResolverJson(MsSqlTestHelper.CharacterListResolver)); + + // Setup Database Components + // + _queryExecutor = new QueryExecutor(MsSqlTestHelper.DataGatewayConfig); + _queryBuilder = new MsSqlQueryBuilder(); + _queryEngine = new SqlQueryEngine(_metadataStoreProvider, _queryExecutor, _queryBuilder); + + // Setup Integration DB Components + // + _databaseInteractor = new DatabaseInteractor(_queryExecutor); + CreateTable(); + InsertData(); + + // Setup GraphQL Components + // + _graphQLService = new GraphQLService(_queryEngine, mutationEngine: null, _metadataStoreProvider); + _graphQLController = new GraphQLController(logger: null, _queryEngine, mutationEngine: null, _graphQLService); + } + + /// + /// Cleans up querying table used for Tests in this class. Only to be run once at + /// conclusion of test run, as defined by MSTest decorator. + /// + [ClassCleanup] + public static void CleanupTestFixture() + { + _databaseInteractor.DropTable(IntegrationTableName); + } + #endregion + #region Tests + /// + /// Get result of quering singular object + /// + /// + [TestMethod] + public async Task SingleResultQuery() + { + string graphQLQueryName = "characterById"; + string graphQLQuery = "{\"query\":\"{\\n characterById(id:2){\\n name\\n primaryFunction\\n}\\n}\\n\"}"; + string msSqlQuery = $"SELECT name, primaryFunction FROM { IntegrationTableName} WHERE id = 2 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER"; + + string actual = await getGraphQLResultAsync(graphQLQuery, graphQLQueryName); + string expected = await getDatabaseResultAsync(msSqlQuery); + + Assert.AreEqual(actual, expected); + } + + /// + /// Gets array of results for querying more than one item. + /// + /// + [TestMethod] + public async Task MultipleResultQuery() + { + string graphQLQueryName = "characterList"; + string graphQLQuery = "{\"query\":\"{\\n characterList {\\n name\\n primaryFunction\\n }\\n}\\n\"}"; + string msSqlQuery = $"SELECT name, primaryFunction FROM character FOR JSON PATH, INCLUDE_NULL_VALUES"; + + string actual = await getGraphQLResultAsync(graphQLQuery, graphQLQueryName); + string expected = await getDatabaseResultAsync(msSqlQuery); + + Assert.AreEqual(actual, expected); + } + #endregion + #region Query Test Helper Functions + /// + /// Sends graphQL query through graphQL service, consisting of gql engine processing (resolvers, object serialization) + /// returning JSON formatted result from 'data' property. + /// + /// + /// + /// string in JSON format + public async Task getGraphQLResultAsync(string graphQLQuery, string graphQLQueryName) + { + _graphQLController.ControllerContext.HttpContext = GetHttpContextWithBody(graphQLQuery); + JsonDocument graphQLResult = await _graphQLController.PostAsync(); + JsonElement graphQLResultData = graphQLResult.RootElement.GetProperty("data").GetProperty(graphQLQueryName); + return graphQLResultData.ToString(); + } + + /// + /// Sends raw SQL query to database engine to retrieve expected result in JSON format + /// + /// raw database query + /// string in JSON format + public async Task getDatabaseResultAsync(string queryText) + { + JsonDocument sqlResult = JsonDocument.Parse("{ }"); + using DbDataReader reader = _databaseInteractor.QueryExecutor.ExecuteQueryAsync(queryText, parameters: null).Result; + + if (await reader.ReadAsync()) + { + sqlResult = JsonDocument.Parse(reader.GetString(0)); + } + + JsonElement sqlResultData = sqlResult.RootElement; + + return sqlResultData.ToString(); + } + #endregion + #region Helper Functions + /// + /// Creates a default table + /// + private static void CreateTable() + { + _databaseInteractor.CreateTable(IntegrationTableName, "id int, name varchar(20), type varchar(20), homePlanet int, primaryFunction varchar(20)"); + } + + /// + /// Inserts some default data into the table + /// + private static void InsertData() + { + _databaseInteractor.InsertData(IntegrationTableName, "'1', 'Mace', 'Jedi','1','Master'"); + _databaseInteractor.InsertData(IntegrationTableName, "'2', 'Plo Koon', 'Jedi','2','Master'"); + _databaseInteractor.InsertData(IntegrationTableName, "'3', 'Yoda', 'Jedi','3','Master'"); + } + /// + /// returns httpcontext with body consisting of GraphQLQuery + /// + /// GraphQLQuery + /// The http context with given data as stream of utf-8 bytes. + private DefaultHttpContext GetHttpContextWithBody(string data) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + var httpContext = new DefaultHttpContext() + { + Request = { Body = stream, ContentLength = stream.Length } + }; + return httpContext; + } + #endregion + } +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlTestHelper.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlTestHelper.cs new file mode 100644 index 0000000000..b0f927fcc0 --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service.Tests/MsSqlTests/MsSqlTestHelper.cs @@ -0,0 +1,69 @@ +using Cosmos.GraphQL.Service.configurations; +using Cosmos.GraphQL.Service.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using System; +using System.IO; + +namespace Cosmos.GraphQL.Service.Tests.MsSql +{ + /// + /// Helper functions for setting up test scenarios + /// + public class MsSqlTestHelper + { + public static readonly string GraphQLSchema = @" + type Query { + characterList: [Character] + characterById (id : ID!): Character + } + type Character { + id : ID, + name : String, + type: String, + homePlanet: Int, + primaryFunction: String + } + "; + + public static readonly string CharacterListResolver = "{\r\n \"id\": \"characterList\",\r\n \"parametrizedQuery\": \"SELECT id, name, type, homePlanet, primaryFunction FROM character\"\r\n }"; + public static readonly string CharacterByIdResolver = "{\r\n \"id\": \"characterById\",\r\n \"parametrizedQuery\": \"SELECT id, name, type, homePlanet, primaryFunction FROM character WHERE id = @id\"\r\n}"; + private static Lazy> _dataGatewayConfig = new Lazy>(() => MsSqlTestHelper.LoadConfig()); + + /// + /// Converts Raw JSON resolver to Resolver class object + /// + /// escaped JSON string + /// GraphQLQueryResolver object + public static GraphQLQueryResolver GetQueryResolverJson(string rawResolverText) + { + return JsonConvert.DeserializeObject(rawResolverText); + } + + /// + /// Sets up configuration object as defined by appsettings.ENV.json file + /// + /// + private static IOptions LoadConfig() + { + DataGatewayConfig datagatewayConfig = new DataGatewayConfig(); + IConfigurationRoot config = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.Test.json") + .Build(); + + config.Bind(nameof(DataGatewayConfig), datagatewayConfig); + + return Options.Create(datagatewayConfig); + } + + /// + /// Returns configuration value loaded from file. + /// + public static IOptions DataGatewayConfig + { + get { return _dataGatewayConfig.Value; } + } + } +} diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/GraphQLService.cs b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/GraphQLService.cs index acc5477564..1e3b03340d 100644 --- a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/GraphQLService.cs +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/Services/GraphQLService.cs @@ -55,7 +55,7 @@ internal async Task ExecuteAsync(String requestBody) IExecutionResult result = await Executor.ExecuteAsync(queryRequest); - return result.ToJson(); + return result.ToJson(withIndentations: false); } private static bool IsIntrospectionPath(IEnumerable path) diff --git a/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSqlIntegrationTest.json b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSqlIntegrationTest.json new file mode 100644 index 0000000000..c5d52f8fbd --- /dev/null +++ b/Cosmos.GraphQL.Service/Cosmos.GraphQL.Service/appsettings.MsSqlIntegrationTest.json @@ -0,0 +1,8 @@ +{ + "DataGatewayConfig": { + "DatabaseType": "MsSql", + "DatabaseConnection": { + "ConnectionString": "Server=tcp:127.0.0.1,1433;Persist Security Info=False;User ID=sa;Password=REPLACEME;MultipleActiveResultSets=False;Connection Timeout=5;" + } + } +}