diff --git a/.jest.js b/.jest.js index 283dceb5..ab7c5053 100644 --- a/.jest.js +++ b/.jest.js @@ -2,6 +2,11 @@ export default { 'transform': {}, 'verbose': true, 'testSequencer': './test/jestTestSequencer.js', + 'testPathIgnorePatterns': [ + '/node_modules/', + // tests below are intended to be executed manually + '.*\\.manual\\.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..6cc7659f 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,78 @@ 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 + +``` +npm run test:manual -- test/appSyncAirRoutesQueries.manual.test.js +``` + +#### Custom AppSync Queries + +``` +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:** + +- 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 + +``` +npm run test:manual -- test/apolloAirRoutesQueries.manual.test.js +``` + +#### Custom Apollo Queries + +``` +npm run test:manual -- test/apolloCustomAirRoutesQueries.manual.test.js +``` + +#### All Apollo Queries + +``` +npm run test:manual -- test/apollo*.manual.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/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/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..d7130e89 --- /dev/null +++ b/test/air-routes-queries.json @@ -0,0 +1,603 @@ +[ + { + "description": "Filter by code", + "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.manual.test.js b/test/apolloAirRoutesQueries.manual.test.js new file mode 100644 index 00000000..a4dc222d --- /dev/null +++ b/test/apolloAirRoutesQueries.manual.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.manual.test.js b/test/apolloCustomAirRoutesQueries.manual.test.js new file mode 100644 index 00000000..c2a8e08d --- /dev/null +++ b/test/apolloCustomAirRoutesQueries.manual.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.manual.test.js b/test/appSyncAirRoutesQueries.manual.test.js new file mode 100644 index 00000000..f6d0e172 --- /dev/null +++ b/test/appSyncAirRoutesQueries.manual.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.manual.test.js b/test/appSyncCustomAirRoutesQueries.manual.test.js new file mode 100644 index 00000000..aab2afee --- /dev/null +++ b/test/appSyncCustomAirRoutesQueries.manual.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..94560629 100644 --- a/test/testLib.js +++ b/test/testLib.js @@ -312,6 +312,70 @@ 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); + expect(response).toEqual({ data: 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 + } + }); + + expect(response.data).toEqual({data: query.expected}); + }, 60000); + }); +} + export { checkFileContains, checkFolderContainsFiles, @@ -323,5 +387,7 @@ export { loadResolver, readJSONFile, testApolloArtifacts, + testApolloQueries, + testAppSyncQueries, testResolverQueriesResults, };