From 482534798ac09324595640fc5d9d74be9e6dd258 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Tue, 9 Sep 2025 15:39:01 -0700 Subject: [PATCH 1/4] Enhanced Testing Infrastructure and Query Coverage: - Added test query files (air-routes-queries.json, custom-air-routes-queries.json) with GraphQL queries covering filters, sorting, pagination, variables, fragments, and custom directives - Added manual integration test files for AppSync and Apollo Server validation which can be executed against an airports API that was deployed using the graphQL utility Additional change to fix the existing Case01 queries which were using the old method of filtering to use the enhanced string comparison filtering. Added changelog. --- .jest.js | 8 + CHANGELOG.md | 2 + TESTING.md | 62 +- test/TestCases/Case01/queries/Query0002.json | 2 +- test/TestCases/Case01/queries/Query0003.json | 2 +- test/TestCases/Case01/queries/Query0004.json | 2 +- test/TestCases/Case01/queries/Query0005.json | 2 +- test/air-routes-changes.json | 38 ++ test/air-routes-queries.json | 603 +++++++++++++++++++ test/apolloAirRoutesQueries.test.js | 8 + test/apolloCustomAirRoutesQueries.test.js | 9 + test/appSyncAirRoutesQueries.test.js | 8 + test/appSyncCustomAirRoutesQueries.test.js | 8 + test/custom-air-routes-queries.json | 154 +++++ test/testLib.js | 82 +++ 15 files changed, 985 insertions(+), 5 deletions(-) create mode 100644 test/air-routes-changes.json create mode 100644 test/air-routes-queries.json create mode 100644 test/apolloAirRoutesQueries.test.js create mode 100644 test/apolloCustomAirRoutesQueries.test.js create mode 100644 test/appSyncAirRoutesQueries.test.js create mode 100644 test/appSyncCustomAirRoutesQueries.test.js create mode 100644 test/custom-air-routes-queries.json diff --git a/.jest.js b/.jest.js index 283dceb5..f181e441 100644 --- a/.jest.js +++ b/.jest.js @@ -2,6 +2,14 @@ export default { 'transform': {}, 'verbose': true, 'testSequencer': './test/jestTestSequencer.js', + 'testPathIgnorePatterns': [ + '/node_modules/', + // tests below are intended to be executed manually + 'appSyncAirRoutesQueries.test.js', + 'appSyncCustomAirRoutesQueries.test.js', + 'apolloAirRoutesQueries.test.js', + 'apolloCustomAirRoutesQueries.test.js' + ], 'globals': { // neptune db that has pre-loaded air routes sample data host and port // ex. db-neptune-foo-bar.cluster-abc.us-west-2.neptune.amazonaws.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 3afdef1c..091028c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,8 @@ permissions and limitations under the License. * Allow credentials to be refreshed at Apollo runtime by passing the credential provider to the interceptor ([#134](https://github.com/aws/amazon-neptune-for-graphql/pull/134)) +* Added manually executed query integration test suites for App Sync and + Apollo ([#138](https://github.com/aws/amazon-neptune-for-graphql/pull/138)) ### Bug Fixes diff --git a/TESTING.md b/TESTING.md index c81811cc..4f49d885 100644 --- a/TESTING.md +++ b/TESTING.md @@ -19,7 +19,7 @@ the following command: npm run test:unit ``` -## Integration Tests +## Automated Integration Tests Integration tests execute against a live neptune db cluster or graph and require the following prerequisites to run: @@ -54,6 +54,66 @@ To execute the integration tests use the following command: npm run test:integration ``` +## Manual Integration Tests + +In addition to the automated integration tests, there are manual integration +tests that can be run against live AWS AppSync APIs and Apollo Server instances +that were deployed via the Amazon Neptune Utility for GraphQL. These tests are +useful for validating deployed GraphQL APIs that are interfacing Neptune db or +analytics graphs loaded with the airports sample data. + +### AppSync Manual Tests + +These tests execute queries against a deployed AWS AppSync API: + +#### Standard AppSync Queries + +``` +node --experimental-vm-modules node_modules/jest/bin/jest.js test/appSyncAirRoutesQueries.test.js +``` + +#### Custom AppSync Queries + +``` +node --experimental-vm-modules node_modules/jest/bin/jest.js test/appSyncCustomAirRoutesQueries.test.js +``` + +**Prerequisites for AppSync tests:** + +- Set environment variables: + ``` + export APP_SYNC_API_ID=your-appsync-api-id + export APP_SYNC_API_KEY=your-api-key + export APP_SYNC_REGION=your-aws-region + ``` + +### Apollo Server Manual Tests + +These tests execute queries against a local Apollo Server instance: + +#### Standard Apollo Queries + +``` +node --experimental-vm-modules node_modules/jest/bin/jest.js test/apolloAirRoutesQueries.test.js +``` + +#### Custom Apollo Queries + +``` +node --experimental-vm-modules node_modules/jest/bin/jest.js test/apolloCustomAirRoutesQueries.test.js +``` + +### Test Query Suites + +Both test suites use JSON files containing GraphQL queries and expected results: + +- **`air-routes-queries.json`**: Standard queries including filters, sorting, + pagination, variables, and fragments +- **`custom-air-routes-queries.json`**: Custom queries using `@graphQuery` + directives, Gremlin queries, and custom field resolvers. These queries assume + that the utility was executed with option + `--input-schema-changes-file ./test/air-routes-changes.json` + ## Loading Airports Sample Data Into Neptune The easiest way to load the airports sample data into Neptune is using diff --git a/test/TestCases/Case01/queries/Query0002.json b/test/TestCases/Case01/queries/Query0002.json index b2d28f69..63b0d8bc 100644 --- a/test/TestCases/Case01/queries/Query0002.json +++ b/test/TestCases/Case01/queries/Query0002.json @@ -1,7 +1,7 @@ { "name": "getAirport _id", "description": "Get neptune _id", - "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n code\n }\n }", + "graphql": "query MyQuery {\n getAirport(filter: {code: {eq: \"SEA\"}}) {\n code\n }\n }", "result":{ "code": "SEA" } diff --git a/test/TestCases/Case01/queries/Query0003.json b/test/TestCases/Case01/queries/Query0003.json index 8c83469f..481c185c 100644 --- a/test/TestCases/Case01/queries/Query0003.json +++ b/test/TestCases/Case01/queries/Query0003.json @@ -2,7 +2,7 @@ "id": "3", "name": "getAirport nested type", "description": "Nested types single and array, references in and out", - "graphql": "query MyQuery {\n getAirport(code: \"YKM\") {\n city\n continentContainsIn {\n desc\n }\n countryContainsIn {\n desc\n }\n airportRoutesOut {\n code\n }\n }\n }", + "graphql": "query MyQuery {\n getAirport(filter: {code: {eq: \"YKM\"}}) {\n city\n continentContainsIn {\n desc\n }\n countryContainsIn {\n desc\n }\n airportRoutesOut {\n code\n }\n }\n }", "result":{ "airportRoutesOut": [ { diff --git a/test/TestCases/Case01/queries/Query0004.json b/test/TestCases/Case01/queries/Query0004.json index 9b2e8bae..c751eeac 100644 --- a/test/TestCases/Case01/queries/Query0004.json +++ b/test/TestCases/Case01/queries/Query0004.json @@ -1,7 +1,7 @@ { "name": "Edge properties 2", "description": "Get edge properties in nested array", - "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n airportRoutesOut {\n code\n route {\n dist\n }\n }\n }\n }\n", + "graphql": "query MyQuery {\n getAirport(filter: {code: {eq: \"SEA\"}}) {\n airportRoutesOut {\n code\n route {\n dist\n }\n }\n }\n }\n", "result": { "airportRoutesOut": [ { diff --git a/test/TestCases/Case01/queries/Query0005.json b/test/TestCases/Case01/queries/Query0005.json index e5b0ea5a..c4d8cca8 100644 --- a/test/TestCases/Case01/queries/Query0005.json +++ b/test/TestCases/Case01/queries/Query0005.json @@ -1,7 +1,7 @@ { "name": "Type graph query 1", "description": "Type with graph query returning a scalar", - "graphql": "query MyQuery {\n getAirport(code: \"YYZ\") {\n outboundRoutesCount\n }\n }\n", + "graphql": "query MyQuery {\n getAirport(filter: {code: {eq: \"YYZ\"}}) {\n outboundRoutesCount\n }\n }\n", "result": { "outboundRoutesCount": 195 } diff --git a/test/air-routes-changes.json b/test/air-routes-changes.json new file mode 100644 index 00000000..1668cb74 --- /dev/null +++ b/test/air-routes-changes.json @@ -0,0 +1,38 @@ +[ + { + "type": "Airport", + "field": "outboundRoutesCount", + "action": "add", + "value": "outboundRoutesCount: Int @graphQuery(statement: \"MATCH (this)-[r:route]->(a) RETURN count(r)\")" + }, + { + "type": "Query", + "field": "getAirportWithGremlin", + "action": "add", + "value": "getAirportWithGremlin(code:String): Airport @graphQuery(statement: \"g.V().has('airport', 'code', '$code').elementMap()\")" + }, + { + "type": "Query", + "field": "getContinentsWithGremlin", + "action": "add", + "value": "getContinentsWithGremlin: [Continent] @cypher(statement: \"g.V().hasLabel('continent').order().elementMap().fold()\")" + }, + { + "type": "Query", + "field": "getAirportByCode", + "action": "add", + "value": "getAirportByCode(code:String): Airport @graphQuery(statement: \"MATCH (this: airport {code: '$code'})\")" + }, + { + "type": "Query", + "field": "getCountriesCountGremlin", + "action": "add", + "value": "getCountriesCountGremlin: Int @graphQuery(statement: \"g.V().hasLabel('country').count()\")" + }, + { + "type": "Query", + "field": "getAirportConnection", + "action": "add", + "value": "getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: \"MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})\")" + } +] \ No newline at end of file diff --git a/test/air-routes-queries.json b/test/air-routes-queries.json new file mode 100644 index 00000000..2ba18be9 --- /dev/null +++ b/test/air-routes-queries.json @@ -0,0 +1,603 @@ +[ + { + "description": "filter", + "query": "query MyQuery { getAirport(filter: {code: {eq: \"SEA\"}}) { city } }", + "expected": { + "getAirport": { + "city": "Seattle" + } + } + }, + { + "description": "Limit in nested edge", + "query": "query MyQuery { getAirport(filter: {code: {eq: \"SEA\"}}) { airportRoutesOut(filter: {country: {eq: \"DE\"}} options: {limit: 3} sort: [{ code: DESC }]) { code } } }", + "expected": { + "getAirport": { + "airportRoutesOut": [ + { + "code": "MUC" + }, + { + "code": "FRA" + }, + { + "code": "CGN" + } + ] + } + } + }, + { + "description": "Filter in nested edge", + "query": "query MyQuery { getAirport(filter: {code: {eq: \"SEA\"}}) { airportRoutesOut(filter: {code: {eq: \"LAX\"}}) { city } city } }", + "expected": { + "getAirport": { + "airportRoutesOut": [ + { + "city": "Los Angeles" + } + ], + "city": "Seattle" + } + } + }, + { + "description": "Mutation: update node: update seattle airport node for neptune-db", + "note": "This query may fail with neptune analytics or have different expected results", + "query": "mutation MyMutation { updateAirport(input: {_id: \"22\", city: \"Seattle\"}) { city } }", + "expected": { + "updateAirport": { + "city": "Seattle" + } + } + }, + { + "description": "Get by _id", + "note": "This query may fail with neptune analytics or have different expected results", + "query": "query MyQuery { getAirport(filter: {_id: \"22\"}) { city } }", + "expected": { + "getAirport": { + "city": "Seattle" + } + } + }, + { + "description": "Limit option", + "query": "query MyQuery { getAirports(options: {limit: 1}, filter: {code: {eq: \"SEA\"}}) { city } }", + "expected": { + "getAirports": [ + { + "city": "Seattle" + } + ] + } + }, + { + "description": "Get different type of fields", + "query": "query MyQuery { getAirport(filter: {code: {eq: \"SEA\"}}) { city elev runways lat lon } }", + "expected": { + "getAirport": { + "city": "Seattle", + "elev": 432, + "lon": -122.30899810791, + "runways": 3, + "lat": 47.4490013122559 + } + } + }, + { + "description": "Filter by parameter with numeric value and return mix of numeric value types", + "query": "query MyQuery { getAirports(filter: { city: {eq: \"Seattle\"}, runways: 3 }) { code lat lon elev } }", + "expected": { + "getAirports": [ + { + "code": "SEA", + "elev": 432, + "lon": -122.30899810791, + "lat": 47.4490013122559 + } + ] + } + }, + { + "description": "Get continents", + "query": "query MyQuery { getContinents(sort: [{ code: DESC }]) { code desc } }", + "expected": { + "getContinents": [ + { + "code": "SA", + "desc": "South America" + }, + { + "code": "OC", + "desc": "Oceania" + }, + { + "code": "NA", + "desc": "North America" + }, + { + "code": "EU", + "desc": "Europe" + }, + { + "code": "AS", + "desc": "Asia" + }, + { + "code": "AN", + "desc": "Antarctica" + }, + { + "code": "AF", + "desc": "Africa" + } + ] + } + }, + { + "description": "Get country with filter", + "query": "query MyQuery { getCountry(filter: {code: {eq: \"CA\"}}) { desc } }", + "expected": { + "getCountry": { + "desc": "Canada" + } + } + }, + { + "description": "Filter with string comparison operators", + "query": "query getAirports { getAirports(filter: { country: { eq: \"CA\" }, code: { startsWith: \"Y\" }, city: { endsWith: \"n\" }, desc: { contains: \"Airport\" } runways: 3 }, options: {limit: 5}, sort: [{ code: ASC }]) { code city country runways desc } }", + "expected": { + "getAirports": [ + { + "city": "Brandon", + "code": "YBR", + "country": "CA", + "desc": "Brandon Municipal Airport", + "runways": 3 + }, + { + "city": "Fort Nelson", + "code": "YYE", + "country": "CA", + "desc": "Fort Nelson Airport", + "runways": 3 + } + ] + } + }, + { + "description": "Nested edge filter with string comparison", + "query": "query getAirports { getAirports(filter: { country: { eq: \"CA\" } }, options: { limit: 5 }, sort: [{ code: ASC }]) { code city country airportRoutesOut(filter: { country: { startsWith: \"C\" }, code: { contains: \"Y\" } }, options: { limit: 3 }, sort: [{ code: ASC }]) { code city country } } }", + "expected": { + "getAirports": [ + { + "airportRoutesOut": [ + { + "city": "Ivujivik", + "code": "YIK", + "country": "CA" + }, + { + "city": "Puvirnituq", + "code": "YPX", + "country": "CA" + } + ], + "city": "Akulivik", + "code": "AKV", + "country": "CA" + }, + { + "airportRoutesOut": [ + { + "city": "Red Lake", + "code": "YRL", + "country": "CA" + } + ], + "city": "Keewaywin", + "code": "KEW", + "country": "CA" + }, + { + "airportRoutesOut": [ + { + "city": "Vancouver", + "code": "YVR", + "country": "CA" + } + ], + "city": "Bella Coola", + "code": "QBC", + "country": "CA" + }, + { + "airportRoutesOut": [ + { + "city": "Sioux Lookout", + "code": "YXL", + "country": "CA" + } + ], + "city": "Kingfisher Lake", + "code": "KIF", + "country": "CA" + }, + { + "airportRoutesOut": [ + { + "city": "Sioux Lookout", + "code": "YXL", + "country": "CA" + } + ], + "city": "Muskrat Dam", + "code": "MSA", + "country": "CA" + } + ] + } + }, + { + "description": "Query with nested edge filter and variables and sort", + "query": "query getAirports($filter: AirportInput, $options: Options, $nestedFilter: AirportInput, $nestedOptions: Options) { getAirports(filter: $filter, options: $options, sort: [{ city: ASC }]) { city code airportRoutesOut(filter: $nestedFilter, options: $nestedOptions, sort: [{ city: ASC }]) { city code } } }", + "variables": { + "filter": { + "country": { + "eq": "CA" + } + }, + "options": { + "limit": 3 + }, + "nestedFilter": { + "country": { + "startsWith": "C" + } + }, + "nestedOptions": { + "limit": 2 + } + }, + "expected": { + "getAirports": [ + { + "airportRoutesOut": [ + { + "city": "Calgary", + "code": "YYC" + }, + { + "city": "Edmonton", + "code": "YEG" + } + ], + "city": "Abbotsford", + "code": "YXX" + }, + { + "airportRoutesOut": [ + { + "city": "Ivujivik", + "code": "YIK" + }, + { + "city": "Puvirnituq", + "code": "YPX" + } + ], + "city": "Akulivik", + "code": "AKV" + }, + { + "airportRoutesOut": [ + { + "city": "Vancouver", + "code": "YVR" + } + ], + "city": "Anahim Lake", + "code": "YAA" + } + ] + } + }, + { + "description": "Query with nested scalar variables and sort", + "query": "query getAirports($country: String, $limit: Int) { getAirports(filter: {country: {eq: $country}}, options: {limit: $limit}, sort: [{ city: ASC }]) { code airportRoutesOut(filter: {country: {eq: $country}}, options: {limit: $limit}, sort: [{ city: ASC }]) { code } } }", + "variables": { + "country": "CA", + "limit": 3 + }, + "expected": { + "getAirports": [ + { + "airportRoutesOut": [ + { + "code": "YYC" + }, + { + "code": "YEG" + }, + { + "code": "YHM" + } + ], + "code": "YXX" + }, + { + "airportRoutesOut": [ + { + "code": "YIK" + }, + { + "code": "YPX" + } + ], + "code": "AKV" + }, + { + "airportRoutesOut": [ + { + "code": "YVR" + } + ], + "code": "YAA" + } + ] + } + }, + { + "description": "Query with sort arguments as a list and limit", + "query": "query MyQuery { getAirports(sort: [{ desc: ASC }, { code: DESC }, { city: DESC }], options: { limit: 3}) { desc code city } }", + "expected": { + "getAirports": [ + { + "city": "Culleredo", + "code": "LCG", + "desc": "A Coruna Airport" + }, + { + "city": "Aalborg", + "code": "AAL", + "desc": "Aalborg Airport" + }, + { + "city": "Aarhus", + "code": "AAR", + "desc": "Aarhus Airport" + } + ] + } + }, + { + "description": "Query with nested sort arguments and limit", + "query": "query MyQuery { getAirports(sort: [{desc: ASC}, {code: DESC}], options: { limit: 3 }) { desc code airportRoutesIn(sort: [{country : ASC}, {city : DESC}], options: { limit: 2 }) { country city } } }", + "expected": { + "getAirports": [ + { + "airportRoutesIn": [ + { + "city": "Prague", + "country": "CZ" + }, + { + "city": "Munich", + "country": "DE" + } + ], + "code": "AAR", + "desc": "Aarhus Airport" + }, + { + "airportRoutesIn": [ + { + "city": "Copenhagen", + "country": "DK" + }, + { + "city": "Billund", + "country": "DK" + } + ], + "code": "AAL", + "desc": "Aalborg Airport" + }, + { + "airportRoutesIn": [ + { + "city": "Tenerife Island", + "country": "ES" + }, + { + "city": "Tenerife", + "country": "ES" + } + ], + "code": "LCG", + "desc": "A Coruna Airport" + } + ] + } + }, + { + "description": "Query with nested sort arguments and variables and limit", + "note": "Executing this query against Apollo may fail as it cannot handle quoted enum values in variables", + "query": "query getAirports($nestedOptions: Options, $nestedSort: [AirportSort!]) { getAirports(options: { limit: 1 }, sort: [ { country: ASC }, { city: ASC } ]) { city code country airportRoutesIn(options: $nestedOptions, sort: $nestedSort) { city code country } } }", + "variables": { + "nestedOptions": { + "limit": 1 + }, + "nestedSort": [ + { + "country": "DESC" + }, + { + "code": "DESC" + } + ] + }, + "expected": { + "getAirports": [ + { + "airportRoutesIn": [ + { + "city": "Johannesburg", + "code": "JNB", + "country": "ZA" + } + ], + "city": "Abu Dhabi", + "code": "AUH", + "country": "AE" + } + ] + } + }, + { + "description": "Query with limit, offset, and sort", + "query": "query MyQuery { getAirports(options: { limit: 4, offset: 3 }, sort: [ { city: ASC } ]) { code } }", + "expected": { + "getAirports": [ + { + "code": "ABD" + }, + { + "code": "ABA" + }, + { + "code": "YXX" + }, + { + "code": "AEH" + } + ] + } + }, + { + "description": "Query with nested edge limit and offset and sort", + "query": "query MyQuery { getAirports(options: { limit: 5, offset: 2 }, sort: [ { city: DESC } ]) { code airportRoutesIn(options: {offset: 5, limit: 3}, sort: [ { city: DESC } ]) { code } } }", + "expected": { + "getAirports": [ + { + "airportRoutesIn": [ + { + "code": "SXB" + }, + { + "code": "ARN" + }, + { + "code": "VAS" + } + ], + "code": "ADB" + }, + { + "airportRoutesIn": [ + { + "code": "NLT" + }, + { + "code": "XNN" + }, + { + "code": "XIY" + } + ], + "code": "URC" + }, + { + "airportRoutesIn": [], + "code": "HOV" + }, + { + "airportRoutesIn": [ + { + "code": "AMS" + } + ], + "code": "LCJ" + }, + { + "airportRoutesIn": [], + "code": "OLA" + } + ] + } + }, + { + "description": "Query with nested edge limit and offset from variables", + "query": "query getAirports($nestedOptions: Options) { getAirports(options: { limit: 2, offset: 1 }, sort: [ { city: DESC } ]) { code airportRoutesOut(options: $nestedOptions, sort: [ { city: DESC } ]) { code } } }", + "variables": { + "nestedOptions": { + "limit": 3, + "offset": 2 + } + }, + "expected": { + "getAirports": [ + { + "airportRoutesOut": [ + { + "code": "CTS" + } + ], + "code": "MMB" + }, + { + "airportRoutesOut": [ + { + "code": "STN" + }, + { + "code": "ATH" + }, + { + "code": "AYT" + } + ], + "code": "LCJ" + } + ] + } + }, + { + "description": "Query with multiple fragments", + "note": "This query will fail with App Sync which does not support fragments", + "query": "fragment locationFields on Airport { city, country } fragment otherFields on Airport { code, elev } query GetAirport { getAirport(filter: {code: {eq: \"YVR\"}}) { ...locationFields ...otherFields runways } }", + "expected": { + "getAirport": { + "city": "Vancouver", + "code": "YVR", + "country": "CA", + "elev": 14, + "runways": 3 + } + } + }, + { + "description": "Query with nested edge fragments", + "note": "This query will fail with App Sync which does not support fragments", + "query": "fragment locationFields on Airport { city, country } fragment otherFields on Airport { code, elev } query GetAirport { getAirport(filter: {code: {eq: \"YVR\"}}) { ...locationFields airportRoutesIn(options: {limit: 2}, sort: [{code: ASC}]) {...locationFields} ...otherFields } }", + "expected": { + "getAirport": { + "airportRoutesIn": [ + { + "city": "Auckland", + "country": "NZ" + }, + { + "city": "Amsterdam", + "country": "NL" + } + ], + "city": "Vancouver", + "code": "YVR", + "country": "CA", + "elev": 14 + } + } + } +] \ No newline at end of file diff --git a/test/apolloAirRoutesQueries.test.js b/test/apolloAirRoutesQueries.test.js new file mode 100644 index 00000000..a4dc222d --- /dev/null +++ b/test/apolloAirRoutesQueries.test.js @@ -0,0 +1,8 @@ +import { testApolloQueries } from "./testLib.js"; + +/** + * Executes a standard set of queries against Apollo Server running on localhost:4000. + */ +describe('Execute Standard Air Routes Queries - Apollo Server', () => { + testApolloQueries('./test/air-routes-queries.json'); +}); diff --git a/test/apolloCustomAirRoutesQueries.test.js b/test/apolloCustomAirRoutesQueries.test.js new file mode 100644 index 00000000..c2a8e08d --- /dev/null +++ b/test/apolloCustomAirRoutesQueries.test.js @@ -0,0 +1,9 @@ +import { testApolloQueries } from "./testLib.js"; + +/** + * Executes queries that are added to the graphQL schema via --input-schema-changes-file air-routes-changes.json. + * Tests against Apollo Server running on localhost:4000. + */ +describe('Execute Custom Air Routes Queries - Apollo Server', () => { + testApolloQueries('./test/custom-air-routes-queries.json'); +}); diff --git a/test/appSyncAirRoutesQueries.test.js b/test/appSyncAirRoutesQueries.test.js new file mode 100644 index 00000000..f6d0e172 --- /dev/null +++ b/test/appSyncAirRoutesQueries.test.js @@ -0,0 +1,8 @@ +import { testAppSyncQueries } from './testLib.js'; + +/** + * Executes a standard set of queries against an AppSync API. + */ +describe('Execute Standard Air Routes Queries', () => { + testAppSyncQueries('./test/air-routes-queries.json'); +}); diff --git a/test/appSyncCustomAirRoutesQueries.test.js b/test/appSyncCustomAirRoutesQueries.test.js new file mode 100644 index 00000000..aab2afee --- /dev/null +++ b/test/appSyncCustomAirRoutesQueries.test.js @@ -0,0 +1,8 @@ +import { testAppSyncQueries } from './testLib.js'; + +/** + * Executes queries that are added to the graphQL schema via --input-schema-changes-file air-routes-changes.json. + */ +describe('Execute Custom Air Routes Queries', () => { + testAppSyncQueries('./test/custom-air-routes-queries.json'); +}); diff --git a/test/custom-air-routes-queries.json b/test/custom-air-routes-queries.json new file mode 100644 index 00000000..d839c109 --- /dev/null +++ b/test/custom-air-routes-queries.json @@ -0,0 +1,154 @@ +[ + { + "description": "getAirport: Inference query from return type", + "query": "query MyQuery {\n getAirportByCode(code: \"SEA\") {\n city \n }\n}", + "expected": { + "getAirportByCode": { + "city": "Seattle" + } + } + }, + { + "id": "3", + "description": "getAirport nested type: Nested types single and array, references in and out", + "query": "query MyQuery {\n getAirportByCode(code: \"YKM\") {\n city\n continentContainsIn {\n desc\n }\n countryContainsIn {\n desc\n }\n airportRoutesOut {\n code\n }\n }\n }", + "expected": { + "getAirportByCode": { + "airportRoutesOut": [ + { + "code": "SEA" + } + ], + "city": "Yakima", + "countryContainsIn": { + "desc": "United States" + }, + "continentContainsIn": { + "desc": "North America" + } + } + } + }, + { + "description": "Edge properties 2: Get edge properties in nested array", + "query": "query MyQuery {\n getAirportByCode(code: \"SEA\") {\n airportRoutesOut(options: {limit: 4} sort: [{ code: DESC }]) {\n code\n route {\n dist\n }\n }\n }\n }\n", + "expected": { + "getAirportByCode": { + "airportRoutesOut": [ + { + "code": "YYZ", + "route": { + "dist": 2053 + } + }, + { + "code": "YYJ", + "route": { + "dist": 97 + } + }, + { + "code": "YYC", + "route": { + "dist": 451 + } + }, + { + "code": "YVR", + "route": { + "dist": 127 + } + } + ] + } + } + }, + { + "description": "Type graph query 1: Type with graph query returning a scalar", + "query": "query MyQuery {\n getAirportByCode(code: \"YYZ\") {\n outboundRoutesCount\n }\n }\n", + "expected": { + "getAirportByCode": { + "outboundRoutesCount": 195 + } + } + }, + { + "description": "Field alias: Map type name to different graph db property name", + "query": "query MyQuery {\n getAirportByCode(code: \"SEA\") {\n desc2: desc\n }\n }\n", + "expected": { + "getAirportByCode": { + "desc2": "Seattle-Tacoma" + } + } + }, + { + "description": "graphQuery type: Query using a graphQuery returning a type", + "query": "query MyQuery {\n getAirportConnection(fromCode: \"YKF\", toCode: \"ORD\") {\n city\n code\n }\n }\n", + "expected": { + "getAirportConnection": { + "code": "YYC", + "city": "Calgary" + } + } + }, + { + "description": "graphQuery Gremlin type: Query using Gremlin returning a type", + "note": "This query may fail with neptune analytics or have different expected results", + "query": "query MyQuery {\n getAirportWithGremlin(code: \"SEA\") {\n _id\n city\n runways\n }\n }\n", + "expected": { + "getAirportWithGremlin": { + "_id": "22", + "city": "Seattle", + "runways": 3 + } + } + }, + { + "description": "graphQuery Gremlin type array: Query using Gremlin returning a type array", + "note": "This query may fail with neptune analytics or have different expected results", + "query": "query MyQuery {\n getContinentsWithGremlin {\n code\n }\n }\n", + "expected": { + "getContinentsWithGremlin": [ + { + "code": "EU" + }, + { + "code": "AF" + }, + { + "code": "NA" + }, + { + "code": "SA" + }, + { + "code": "AS" + }, + { + "code": "OC" + }, + { + "code": "AN" + } + ] + } + }, + { + "description": "graphQuery Gremlin scalar: Query using Gremlin returning a scalar", + "note": "This query may fail with neptune analytics or have different expected results", + "query": "query MyQuery {\n getCountriesCountGremlin\n }\n", + "expected": { + "getCountriesCountGremlin": 237 + } + }, + { + "description": "outboundRoutesCount: Custom field with graph query returning a scalar", + "note": "This query may produce a different count with neptune analytics vs neptune db", + "query": "query MyQuery {\n getAirportByCode(code: \"SEA\") {\n outboundRoutesCount\n }\n }", + "expected": { + "getAirportByCode": { + "outboundRoutesCount": 122 + } + } + } +] \ No newline at end of file diff --git a/test/testLib.js b/test/testLib.js index 9581ba3b..4e153320 100644 --- a/test/testLib.js +++ b/test/testLib.js @@ -312,6 +312,86 @@ async function executeAppSyncQuery(apiId, query, variables, region, apiKey) { return executeGraphQLQuery(apiId, apiKey, query, variables, region); } +/** + * Executes set of queries from a json query file against an AppSync API. + * The following environment variables are executed to be set: APP_SYNC_API_ID, APP_SYNC_API_KEY, APP_SYNC_REGION. + * + * @param queryFilePath the json file which contains the queries to execute + */ +function testAppSyncQueries(queryFilePath) { + const apiId = process.env.APP_SYNC_API_ID; + if (!apiId) { + throw new Error('Expected environment variable APP_SYNC_API_ID is not set'); + } + const apiKey = process.env.APP_SYNC_API_KEY; + if (!apiKey) { + throw new Error('Expected environment variable APP_SYNC_API_KEY is not set'); + } + const region = process.env.APP_SYNC_REGION; + if (!region) { + throw new Error('Expected environment variable APP_SYNC_REGION is not set'); + } + + const queries = JSON.parse(fs.readFileSync(queryFilePath, 'utf8')); + + queries.forEach((query, _) => { + test(`App Sync Query: ${query.description}`, async () => { + if (query.note) { + console.log(query.note); + } + const response = await executeGraphQLQuery(apiId, apiKey, query.query, query.variables || {}, region); + const success = !response.errors; + if (!success) { + console.log(`Query failed: ${query.description}`); + console.log(`Response: ${JSON.stringify(response, null, 2)}`); + } + expect(success).toBe(true); + if (query.expected) { + expect(response.data).toEqual(query.expected); + } + }, 60000); + }); +} + +/** + * Executes set of queries from a json query file against an Apollo Server running on localhost:4000. + * + * @param queryFilePath the json file which contains the queries to execute + */ +function testApolloQueries(queryFilePath) { + const apolloUrl = 'http://localhost:4000/graphql'; + const queries = JSON.parse(fs.readFileSync(queryFilePath, 'utf8')); + + queries.forEach((query, _) => { + test(`Apollo Query: ${query.description}`, async () => { + if (query.note) { + console.log(query.note); + } + const response = await axios({ + url: apolloUrl, + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: { + query: query.query, + variables: query.variables + } + }); + + const success = !response.data.errors; + if (!success) { + console.log(`Query failed: ${query.description}`); + console.log(`Response Errors: ${JSON.stringify(response.data.errors, null, 2)}`); + } + expect(success).toBe(true); + if (query.expected) { + expect(response.data.data).toEqual(query.expected); + } + }, 60000); + }); +} + export { checkFileContains, checkFolderContainsFiles, @@ -323,5 +403,7 @@ export { loadResolver, readJSONFile, testApolloArtifacts, + testApolloQueries, + testAppSyncQueries, testResolverQueriesResults, }; From bc7ae546949fa50777142104b4ef4a1e09acef26 Mon Sep 17 00:00:00 2001 From: andreachild Date: Fri, 12 Sep 2025 10:57:55 -0700 Subject: [PATCH 2/4] Update test/testLib.js Co-authored-by: Kris McGinnes --- test/testLib.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/test/testLib.js b/test/testLib.js index 4e153320..e15ccfd2 100644 --- a/test/testLib.js +++ b/test/testLib.js @@ -340,15 +340,7 @@ function testAppSyncQueries(queryFilePath) { console.log(query.note); } const response = await executeGraphQLQuery(apiId, apiKey, query.query, query.variables || {}, region); - const success = !response.errors; - if (!success) { - console.log(`Query failed: ${query.description}`); - console.log(`Response: ${JSON.stringify(response, null, 2)}`); - } - expect(success).toBe(true); - if (query.expected) { - expect(response.data).toEqual(query.expected); - } + expect(response).toEqual({ data: query.expected }); }, 60000); }); } From ecc3674de65fb34162e1d4fce92daf414c912f14 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Fri, 12 Sep 2025 12:12:37 -0700 Subject: [PATCH 3/4] Rename manual test files and create script to help execution of manual tests. Simplified response verification. --- .jest.js | 5 +---- TESTING.md | 20 +++++++++++++++---- package.json | 3 ++- ... => apolloAirRoutesQueries.manual.test.js} | 0 ...olloCustomAirRoutesQueries.manual.test.js} | 0 ...=> appSyncAirRoutesQueries.manual.test.js} | 0 ...SyncCustomAirRoutesQueries.manual.test.js} | 0 test/testLib.js | 10 +--------- 8 files changed, 20 insertions(+), 18 deletions(-) rename test/{apolloAirRoutesQueries.test.js => apolloAirRoutesQueries.manual.test.js} (100%) rename test/{apolloCustomAirRoutesQueries.test.js => apolloCustomAirRoutesQueries.manual.test.js} (100%) rename test/{appSyncAirRoutesQueries.test.js => appSyncAirRoutesQueries.manual.test.js} (100%) rename test/{appSyncCustomAirRoutesQueries.test.js => appSyncCustomAirRoutesQueries.manual.test.js} (100%) diff --git a/.jest.js b/.jest.js index f181e441..ab7c5053 100644 --- a/.jest.js +++ b/.jest.js @@ -5,10 +5,7 @@ export default { 'testPathIgnorePatterns': [ '/node_modules/', // tests below are intended to be executed manually - 'appSyncAirRoutesQueries.test.js', - 'appSyncCustomAirRoutesQueries.test.js', - 'apolloAirRoutesQueries.test.js', - 'apolloCustomAirRoutesQueries.test.js' + '.*\\.manual\\.test\\.js$' ], 'globals': { // neptune db that has pre-loaded air routes sample data host and port diff --git a/TESTING.md b/TESTING.md index 4f49d885..6cc7659f 100644 --- a/TESTING.md +++ b/TESTING.md @@ -69,13 +69,19 @@ These tests execute queries against a deployed AWS AppSync API: #### Standard AppSync Queries ``` -node --experimental-vm-modules node_modules/jest/bin/jest.js test/appSyncAirRoutesQueries.test.js +npm run test:manual -- test/appSyncAirRoutesQueries.manual.test.js ``` #### Custom AppSync Queries ``` -node --experimental-vm-modules node_modules/jest/bin/jest.js test/appSyncCustomAirRoutesQueries.test.js +npm run test:manual -- test/appSyncCustomAirRoutesQueries.manual.test.js +``` + +#### All AppSync Queries + +``` +npm run test:manual -- test/appSync*.manual.test.js ``` **Prerequisites for AppSync tests:** @@ -94,13 +100,19 @@ These tests execute queries against a local Apollo Server instance: #### Standard Apollo Queries ``` -node --experimental-vm-modules node_modules/jest/bin/jest.js test/apolloAirRoutesQueries.test.js +npm run test:manual -- test/apolloAirRoutesQueries.manual.test.js ``` #### Custom Apollo Queries ``` -node --experimental-vm-modules node_modules/jest/bin/jest.js test/apolloCustomAirRoutesQueries.test.js +npm run test:manual -- test/apolloCustomAirRoutesQueries.manual.test.js +``` + +#### All Apollo Queries + +``` +npm run test:manual -- test/apollo*.manual.test.js ``` ### Test Query Suites diff --git a/package.json b/package.json index 89177474..6531df6e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=src/test", "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases", "test:sdk": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case07", - "test:resolver": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case01" + "test:resolver": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case01", + "test:manual": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "jest": { "collectCoverage": true, diff --git a/test/apolloAirRoutesQueries.test.js b/test/apolloAirRoutesQueries.manual.test.js similarity index 100% rename from test/apolloAirRoutesQueries.test.js rename to test/apolloAirRoutesQueries.manual.test.js diff --git a/test/apolloCustomAirRoutesQueries.test.js b/test/apolloCustomAirRoutesQueries.manual.test.js similarity index 100% rename from test/apolloCustomAirRoutesQueries.test.js rename to test/apolloCustomAirRoutesQueries.manual.test.js diff --git a/test/appSyncAirRoutesQueries.test.js b/test/appSyncAirRoutesQueries.manual.test.js similarity index 100% rename from test/appSyncAirRoutesQueries.test.js rename to test/appSyncAirRoutesQueries.manual.test.js diff --git a/test/appSyncCustomAirRoutesQueries.test.js b/test/appSyncCustomAirRoutesQueries.manual.test.js similarity index 100% rename from test/appSyncCustomAirRoutesQueries.test.js rename to test/appSyncCustomAirRoutesQueries.manual.test.js diff --git a/test/testLib.js b/test/testLib.js index e15ccfd2..94560629 100644 --- a/test/testLib.js +++ b/test/testLib.js @@ -371,15 +371,7 @@ function testApolloQueries(queryFilePath) { } }); - const success = !response.data.errors; - if (!success) { - console.log(`Query failed: ${query.description}`); - console.log(`Response Errors: ${JSON.stringify(response.data.errors, null, 2)}`); - } - expect(success).toBe(true); - if (query.expected) { - expect(response.data.data).toEqual(query.expected); - } + expect(response.data).toEqual({data: query.expected}); }, 60000); }); } From 1857e3af15141d5e4108a0d34e992086c3f67419 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Fri, 12 Sep 2025 12:15:52 -0700 Subject: [PATCH 4/4] Changed query description to be more descriptive. --- test/air-routes-queries.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/air-routes-queries.json b/test/air-routes-queries.json index 2ba18be9..d7130e89 100644 --- a/test/air-routes-queries.json +++ b/test/air-routes-queries.json @@ -1,6 +1,6 @@ [ { - "description": "filter", + "description": "Filter by code", "query": "query MyQuery { getAirport(filter: {code: {eq: \"SEA\"}}) { city } }", "expected": { "getAirport": {