diff --git a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs index 9621df58e9..f3abe6e78a 100644 --- a/DataGateway.Service.Tests/CosmosTests/MutationTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/MutationTests.cs @@ -81,7 +81,7 @@ public async Task CanCreateItemWithoutVariables() name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, new()); + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); @@ -100,7 +100,7 @@ public async Task CanDeleteItemWithoutVariables() name }} }}"; - _ = await ExecuteGraphQLRequestAsync("addPlanet", addMutation, new()); + _ = await ExecuteGraphQLRequestAsync("addPlanet", addMutation, variables: new()); // Run mutation delete item; string deleteMutation = $@" @@ -110,12 +110,43 @@ public async Task CanDeleteItemWithoutVariables() name }} }}"; - JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", deleteMutation, new()); + JsonElement response = await ExecuteGraphQLRequestAsync("deletePlanet", deleteMutation, variables: new()); // Validate results Assert.IsNull(response.GetProperty("id").GetString()); } + [TestMethod] + public async Task MutationMissingInputReturnError() + { + // Run mutation Add planet without any input + string mutation = $@" +mutation {{ + addPlanet {{ + id + name + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); + Assert.AreEqual("inputDict is missing", response[0].GetProperty("message").ToString()); + } + + [TestMethod] + public async Task MutationMissingRequiredIdReturnError() + { + // Run mutation Add planet without id + const string name = "test_name"; + string mutation = $@" +mutation {{ + addPlanet ( name: ""{name}"") {{ + id + name + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("addPlanet", mutation, variables: new()); + Assert.AreEqual("id field is mandatory", response[0].GetProperty("message").ToString()); + } + /// /// Runs once after all tests in this class are executed /// diff --git a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs index 8d111d7c1f..9445926038 100644 --- a/DataGateway.Service.Tests/CosmosTests/QueryTests.cs +++ b/DataGateway.Service.Tests/CosmosTests/QueryTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -69,10 +70,14 @@ public async Task GetPaginatedWithVariables() // Run query JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); int actualElements = response.GetArrayLength(); + List responseTotal = new(); + ConvertJsonElementToStringList(response, responseTotal); + // Run paginated query int totalElementsFromPaginatedQuery = 0; string continuationToken = null; const int pagesize = 5; + List pagedResponse = new(); do { @@ -80,10 +85,12 @@ public async Task GetPaginatedWithVariables() JsonElement continuation = page.GetProperty("endCursor"); continuationToken = continuation.ToString(); totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); + ConvertJsonElementToStringList(page.GetProperty("items"), pagedResponse); } while (!string.IsNullOrEmpty(continuationToken)); // Validate results Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); + Assert.IsTrue(responseTotal.SequenceEqual(pagedResponse)); } [TestMethod] @@ -110,10 +117,14 @@ public async Task GetPaginatedWithoutVariables() // Run query JsonElement response = await ExecuteGraphQLRequestAsync("planetList", PlanetListQuery); int actualElements = response.GetArrayLength(); + List responseTotal = new(); + ConvertJsonElementToStringList(response, responseTotal); + // Run paginated query int totalElementsFromPaginatedQuery = 0; string continuationToken = null; const int pagesize = 5; + List pagedResponse = new(); do { @@ -129,14 +140,123 @@ public async Task GetPaginatedWithoutVariables() }} }}"; - JsonElement page = await ExecuteGraphQLRequestAsync("planets", planetConnectionQueryStringFormat, new()); + JsonElement page = await ExecuteGraphQLRequestAsync("planets", planetConnectionQueryStringFormat, variables: new()); JsonElement continuation = page.GetProperty("endCursor"); continuationToken = continuation.ToString(); totalElementsFromPaginatedQuery += page.GetProperty("items").GetArrayLength(); + ConvertJsonElementToStringList(page.GetProperty("items"), pagedResponse); } while (!string.IsNullOrEmpty(continuationToken)); // Validate results Assert.AreEqual(actualElements, totalElementsFromPaginatedQuery); + Assert.IsTrue(responseTotal.SequenceEqual(pagedResponse)); + } + + /// + /// Query List Type with input parameters + /// + /// + [TestMethod] + public async Task GetListTypeWithParameters() + { + string id = _idList[0]; + string query = @$" +query {{ + getPlanetListById (id: ""{id}"") {{ + id + name + }} +}}"; + + JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetListById", query); + + // Validate results + Assert.AreEqual(1, response.GetArrayLength()); + Assert.AreEqual(id, response[0].GetProperty("id").ToString()); + } + + /// + /// Query single item by non-primary key field, found no match + /// + /// + [TestMethod] + public async Task GetByNonePrimaryFieldResultNotFound() + { + string name = "non-existed name"; + string query = @$" +query {{ + getPlanetByName (name: ""{name}"") {{ + id + name + }} +}}"; + + JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetByName", query); + + // Validate results + Assert.IsNull(response.Deserialize()); + } + + /// + /// Query single item by non-primary key field, found record back + /// + /// + [TestMethod] + public async Task GetByNonPrimaryFieldReturnsResult() + { + string name = "test name"; + string query = @$" +query {{ + getPlanetByName (name: ""{name}"") {{ + id + name + }} +}}"; + + JsonElement response = await ExecuteGraphQLRequestAsync("getPlanetByName", query); + + // Validate results + Assert.AreEqual(name, response.GetProperty("name").ToString()); + } + + /// + /// Query result with nested object + /// + /// + [TestMethod] + public async Task GetByPrimaryKeyWithInnerObject() + { + // Run query + string id = _idList[0]; + string query = @$" +query {{ + planetById (id: ""{id}"") {{ + id + name + character {{ + id + name + }} + }} +}}"; + JsonElement response = await ExecuteGraphQLRequestAsync("planetById", query); + + // Validate results + Assert.AreEqual(id, response.GetProperty("id").GetString()); + } + + private static void ConvertJsonElementToStringList(JsonElement ele, List strList) + { + if (ele.ValueKind == JsonValueKind.Array) + { + JsonElement.ArrayEnumerator enumerator = ele.EnumerateArray(); + + while (enumerator.MoveNext()) + { + JsonElement prop = enumerator.Current; + strList.Add(prop.ToString()); + } + } } /// diff --git a/DataGateway.Service.Tests/CosmosTests/TestBase.cs b/DataGateway.Service.Tests/CosmosTests/TestBase.cs index 1d4239266f..81ff8046b0 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestBase.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Security.Claims; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -42,6 +43,8 @@ type Query { getPlanet(id: ID, name: String): Planet planetList: [Planet] planets(first: Int, after: String): PlanetConnection + getPlanetListById(id: ID): [Planet] + getPlanetByName(name: String): Planet } type Mutation { @@ -65,7 +68,8 @@ type Character { type Planet { id : ID, - name : String + name : String, + character: Character }"; _metadataStoreProvider.GraphQLSchema = jsonString; @@ -109,9 +113,11 @@ private static DefaultHttpContext GetHttpContextWithBody(string data) HttpRequestMessage request = new(); MemoryStream stream = new(Encoding.UTF8.GetBytes(data)); request.Method = HttpMethod.Post; + ClaimsPrincipal user = new(new ClaimsIdentity(authenticationType: "Bearer")); DefaultHttpContext httpContext = new() { - Request = { Body = stream, ContentLength = stream.Length } + Request = { Body = stream, ContentLength = stream.Length }, + User = user }; return httpContext; } @@ -182,7 +188,8 @@ internal static async Task ExecuteGraphQLRequestAsync(string queryN if (graphQLResult.TryGetProperty("errors", out JsonElement errors)) { - Assert.Fail(errors.GetRawText()); + // to validate expected errors and error message + return errors; } return graphQLResult.GetProperty("data").GetProperty(queryName); diff --git a/DataGateway.Service.Tests/CosmosTests/TestHelper.cs b/DataGateway.Service.Tests/CosmosTests/TestHelper.cs index a2bf916320..6ec6563fde 100644 --- a/DataGateway.Service.Tests/CosmosTests/TestHelper.cs +++ b/DataGateway.Service.Tests/CosmosTests/TestHelper.cs @@ -43,6 +43,7 @@ public static object GetItem(string id) return new { id = id, + name = "test name", myProp = "a value", myIntProp = 4, myBooleanProp = true, @@ -56,6 +57,14 @@ public static object GetItem(string id) lastName = "the last name", zipCode = 784298 } + }, + character = new + { + id = id, + name = "planet character", + type = "Mars", + homePlanet = 1, + primaryFunction = "test function" } }; } diff --git a/DataGateway.Service/Resolvers/CosmosMutationEngine.cs b/DataGateway.Service/Resolvers/CosmosMutationEngine.cs index bdba73fbf6..66ee4e18ba 100644 --- a/DataGateway.Service/Resolvers/CosmosMutationEngine.cs +++ b/DataGateway.Service/Resolvers/CosmosMutationEngine.cs @@ -32,7 +32,7 @@ private async Task ExecuteAsync(IDictionary inputDict, JObject jObject; - if (inputDict != null) + if (inputDict != null && inputDict.Count > 0) { // TODO: optimize this multiple round of serialization/deserialization string json = JsonConvert.SerializeObject(inputDict); @@ -75,7 +75,7 @@ private async Task ExecuteAsync(IDictionary inputDict, break; default: - throw new NotSupportedException($"unsupprted operation type: {resolver.OperationType.ToString()}"); + throw new NotSupportedException($"unsupported operation type: {resolver.OperationType.ToString()}"); } return response.Resource; diff --git a/DataGateway.Service/Resolvers/CosmosQueryEngine.cs b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs index dd32aec7e4..9a881167ea 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryEngine.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryEngine.cs @@ -1,6 +1,7 @@ # nullable disable using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -67,49 +68,48 @@ public async Task> ExecuteAsync(IMiddlewareContex requestContinuation = Base64Decode(structure.Continuation); } - FeedResponse firstPage = await container.GetItemQueryIterator(querySpec, requestContinuation, queryRequestOptions).ReadNextAsync(); - - if (structure.IsPaginated) + using (FeedIterator query = container.GetItemQueryIterator(querySpec, requestContinuation, queryRequestOptions)) { - JArray jarray = new(); - IEnumerator enumerator = firstPage.GetEnumerator(); - while (enumerator.MoveNext()) + do { - JObject item = enumerator.Current; - jarray.Add(item); + FeedResponse page = await query.ReadNextAsync(); + + // For connection type, return first page result directly + if (structure.IsPaginated) + { + JArray jarray = new(); + IEnumerator enumerator = page.GetEnumerator(); + while (enumerator.MoveNext()) + { + JObject item = enumerator.Current; + jarray.Add(item); + } + + string responseContinuation = page.ContinuationToken; + if (string.IsNullOrEmpty(responseContinuation)) + { + responseContinuation = null; + } + + JObject res = new( + new JProperty("endCursor", Base64Encode(responseContinuation)), + new JProperty("hasNextPage", responseContinuation != null), + new JProperty("items", jarray)); + + // This extra deserialize/serialization will be removed after moving to Newtonsoft from System.Text.Json + return new Tuple(JsonDocument.Parse(res.ToString()), null); + } + + if (page.Count > 0) + { + return new Tuple(JsonDocument.Parse(page.First().ToString()), null); + } } - - string responseContinuation = firstPage.ContinuationToken; - if (string.IsNullOrEmpty(responseContinuation)) - { - responseContinuation = null; - } - - JObject res = new( - new JProperty("endCursor", Base64Encode(responseContinuation)), - new JProperty("hasNextPage", responseContinuation != null), - new JProperty("items", jarray)); - - // This extra deserialize/serialization will be removed after moving to Newtonsoft from System.Text.Json - return new Tuple(JsonDocument.Parse(res.ToString()), null); + while (query.HasMoreResults); } - static JObject FindFirstItem(IEnumerator iterator) - { - JObject firstItem; - if (iterator.MoveNext() && (firstItem = iterator.Current) == null) - { - return FindFirstItem(iterator); - } - - return iterator.Current; - } - - JObject firstItem = FindFirstItem(firstPage.GetEnumerator()); - - JsonDocument jsonDocument = JsonDocument.Parse(firstItem.ToString()); - - return new Tuple(jsonDocument, null); + // Return empty list when query gets no result back + return new Tuple(null, null); } public async Task, IMetadata>> ExecuteListAsync(IMiddlewareContext context, IDictionary parameters) @@ -124,12 +124,9 @@ public async Task, IMetadata>> ExecuteListAsync( Container container = _clientProvider.Client.GetDatabase(structure.Database).GetContainer(structure.Container); QueryDefinition querySpec = new(_queryBuilder.Build(structure)); - if (parameters != null) + foreach (KeyValuePair parameterEntry in structure.Parameters) { - foreach (KeyValuePair parameterEntry in parameters) - { - querySpec.WithParameter("@" + parameterEntry.Key, parameterEntry.Value); - } + querySpec.WithParameter("@" + parameterEntry.Key, parameterEntry.Value); } FeedIterator resultSetIterator = container.GetItemQueryIterator(querySpec); diff --git a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs index 6b9aaedd9f..d033b6b048 100644 --- a/DataGateway.Service/Resolvers/CosmosQueryStructure.cs +++ b/DataGateway.Service/Resolvers/CosmosQueryStructure.cs @@ -46,12 +46,12 @@ private void Init(IDictionary queryParams) if (fieldNode != null) { - Columns.AddRange(fieldNode.SelectionSet!.Selections.Select(x => new LabelledColumn(_containerAlias, "", x.ToString()))); + Columns.AddRange(fieldNode.SelectionSet!.Selections.Select(x => new LabelledColumn(_containerAlias, "", x.GetNodes().First().ToString()))); } } else { - Columns.AddRange(selection.SyntaxNode.SelectionSet!.Selections.Select(x => new LabelledColumn(_containerAlias, "", x.ToString()))); + Columns.AddRange(selection.SyntaxNode.SelectionSet!.Selections.Select(x => new LabelledColumn(_containerAlias, "", x.GetNodes().First().ToString()))); } Container = graphqlType.ContainerName; diff --git a/DataGateway.Service/Services/GraphQLService.cs b/DataGateway.Service/Services/GraphQLService.cs index 6a4e324335..3d2731b8f4 100644 --- a/DataGateway.Service/Services/GraphQLService.cs +++ b/DataGateway.Service/Services/GraphQLService.cs @@ -117,6 +117,7 @@ private void MakeSchemaExecutable() { Console.Error.WriteLine(error.Exception.Message); Console.Error.WriteLine(error.Exception.StackTrace); + return error.WithMessage(error.Exception.Message); } return error;