graphql-atg
Advanced tools
Comparing version 1.0.13 to 1.1.0
@@ -9,6 +9,3 @@ import { createClient } from '../../infrastructure/graphql/client.js'; | ||
const allQueries = await generateGraphQLQueries(introspectionResult, config.generation); | ||
return await executeQueries(client, allQueries, { | ||
concurrency: 3, | ||
failFast: false, | ||
}); | ||
return await executeQueries(client, allQueries, [], config.runner); | ||
} |
@@ -9,3 +9,3 @@ /* eslint-disable functional/no-this-expression */ | ||
super(`GraphQL request failed with errors | ||
${details.response.errors.map((error) => ` - ${error.message}`)} | ||
${(details.response?.errors || []).map((error) => ` - ${error.message}`)} | ||
@@ -12,0 +12,0 @@ Query: ${tty ? minify(details.query.query) : prettify(details.query.query)} |
import { newMultiTask } from '../../../core/task/task.js'; | ||
import _ from 'lodash'; | ||
import { FailedGraphQLRequestError } from './error.js'; | ||
export var QueryExecutionStatus; | ||
(function (QueryExecutionStatus) { | ||
QueryExecutionStatus["SUCCESSFUL"] = "SUCCESSFUL"; | ||
QueryExecutionStatus["FAILED"] = "FAILED"; | ||
QueryExecutionStatus["SKIPPED"] = "SKIPPED"; | ||
})(QueryExecutionStatus || (QueryExecutionStatus = {})); | ||
const DEFAULT_RUNNER_CONFIG = { | ||
@@ -7,3 +14,3 @@ concurrency: 1, | ||
}; | ||
export async function executeQueries(client, queries, config) { | ||
export async function executeQueries(client, queries, hooks, config) { | ||
const actualConfig = Object.assign({}, DEFAULT_RUNNER_CONFIG, config); | ||
@@ -13,3 +20,3 @@ const task = newMultiTask(queries.map((query, index) => { | ||
name: `Query ${index + 1}`, | ||
run: async () => await runQuery(query, client), | ||
run: async (context) => await runQuery(index, query, client, context, hooks), | ||
}; | ||
@@ -21,24 +28,19 @@ }), { | ||
}); | ||
try { | ||
const multiTaskResult = await task.start(); | ||
const allDetails = [ | ||
...multiTaskResult.results, | ||
...multiTaskResult.errors.map((error) => convertError(error)), | ||
]; | ||
return { | ||
resultDetails: allDetails, | ||
failed: allDetails.filter((result) => !result.isSuccessful).length, | ||
successful: allDetails.filter((result) => result.isSuccessful).length, | ||
executionTimeMilliseconds: allDetails.reduce((previous, current) => previous + current.executionTimeMilliseconds, 0), | ||
}; | ||
} | ||
catch (error) { | ||
const converted = convertError(error); | ||
return { | ||
executionTimeMilliseconds: converted.executionTimeMilliseconds, | ||
failed: 1, | ||
successful: 0, | ||
resultDetails: [converted], | ||
}; | ||
} | ||
const multiTaskResult = await task.start(); | ||
const results = multiTaskResult.results; | ||
const errors = multiTaskResult.errors.map((error) => convertError(error)); | ||
const allExpectedIndexes = queries.map((_, index) => index); | ||
const allReceivedIndexes = [...results, ...errors].map((detail) => detail.index); | ||
const allDetails = _.sortBy([ | ||
...results, | ||
...errors, | ||
..._.difference(allExpectedIndexes, allReceivedIndexes).map((index) => createSkippedResult(index, queries[index])), | ||
], (detail) => detail.index); | ||
return { | ||
resultDetails: allDetails, | ||
failed: allDetails.filter((result) => result.status === QueryExecutionStatus.FAILED).length, | ||
successful: allDetails.filter((result) => result.status === QueryExecutionStatus.SUCCESSFUL).length, | ||
skipped: allDetails.filter((result) => result.status === QueryExecutionStatus.SKIPPED).length, | ||
executionTimeMilliseconds: allDetails.reduce((previous, current) => previous + (current.executionTimeMilliseconds || 0), 0), | ||
}; | ||
} | ||
@@ -51,15 +53,42 @@ function convertError(error) { | ||
} | ||
async function runQuery(query, client) { | ||
function createSkippedResult(index, query) { | ||
return { | ||
status: QueryExecutionStatus.SKIPPED, | ||
query, | ||
index, | ||
response: undefined, | ||
executionTimeMilliseconds: undefined, | ||
}; | ||
} | ||
async function notifyHooks(hooks, context, extractor) { | ||
return await Promise.all(hooks.map(async (hook) => { | ||
const callback = extractor(hook); | ||
if (callback) { | ||
return await callback(context); | ||
} | ||
})); | ||
} | ||
async function runQuery(index, query, client, context, hooks) { | ||
const hookContext = { | ||
query: query, | ||
task: context, | ||
}; | ||
notifyHooks(hooks, hookContext, (hook) => hook.beforeTest); | ||
const before = new Date().getUTCMilliseconds(); | ||
const result = await client.request(query.query, query.variables); | ||
const details = { | ||
index: index, | ||
executionTimeMilliseconds: new Date().getUTCMilliseconds() - before, | ||
isSuccessful: result.errors.length === 0, | ||
status: result.errors.length === 0 | ||
? QueryExecutionStatus.SUCCESSFUL | ||
: QueryExecutionStatus.FAILED, | ||
query: query, | ||
response: result, | ||
}; | ||
if (result.errors.length > 0) { | ||
if (details.status !== QueryExecutionStatus.SUCCESSFUL) { | ||
notifyHooks(hooks, hookContext, (hook) => hook.onFail); | ||
throw new FailedGraphQLRequestError(details); | ||
} | ||
notifyHooks(hooks, hookContext, (hook) => hook.onSuccess); | ||
return details; | ||
} |
@@ -8,8 +8,8 @@ import getPackageVersion from '@jsbits/get-package-version'; | ||
.version(getPackageVersion()) | ||
.requiredOption('-e, --endpoint <endpoint>', 'The GraphQL endpoint to test against. ') | ||
.requiredOption('-e, --endpoint <endpoint>', 'The GraphQL endpoint to test against.') | ||
.option('-h, --header <header>=<value>', 'Additional headers to add to the GraphQL requests. This can be used for authorization for instance. By doing --header "Authorization=Bearer <token>". This option can be repeated more than once.', convertHeader, {}) | ||
.option('-iid, --introspection.include-deprecated', 'Wether or not the introspection should include the deprecated fields or not', true) | ||
.option('-iid, --introspection.include-deprecated', 'Wether or not the introspection should include the deprecated fields or not.', true) | ||
.option('-gmd, --generation.max-depth <number>', 'The max depth at which the query generation engine will go to generate queries. Every field over this depth will not be queried, so make sure to put a depth as big as necessary for your entire API can be queried.', validatedParseInt, 5) | ||
.option('-gff, --generation.factories-file <file>', 'A GraphQL input type factory file configuration. This javascript file will be imported and executed to override the default factories provided by the framework.', convertToFactoriesFile, []) | ||
.addOption(new Option('-gns, --generation.null-strategy <strategy>', 'Allow specifying if the default behaviour for nullable input values when there is no factory provided is to always use null values, sometimes use null values, or never use null values') | ||
.addOption(new Option('-gns, --generation.null-strategy <strategy>', 'Allow specifying if the default behaviour for nullable input values when there is no factory provided is to always use null values, sometimes use null values, or never use null values.') | ||
.choices([ | ||
@@ -21,4 +21,4 @@ NullGenerationStrategy.NEVER_NULL, | ||
.default(NullGenerationStrategy.NEVER_NULL)) | ||
.option('-rc, --runner.concurrency', 'The number of parallel queries to execute', validatedParseInt, 1) | ||
.option('-rff, --runner.fail-fast', 'Either the tests should stop after the first error is encountered, or keep running until all queries have been executed', false); | ||
.option('-rc, --runner.concurrency', 'The number of parallel queries to execute.', validatedParseInt, 1) | ||
.option('-rff, --runner.fail-fast', 'Either the tests should stop after the first error is encountered, or keep running until all queries have been executed.', false); | ||
program.parse(process.argv); | ||
@@ -25,0 +25,0 @@ const options = program.opts(); |
@@ -67,9 +67,17 @@ /* eslint-disable functional/immutable-data */ | ||
start: async () => { | ||
const context = await tasks.run(); | ||
return { | ||
results: context.results, | ||
errors: tasks.err.map((err) => err.error), | ||
}; | ||
try { | ||
const context = await tasks.run(); | ||
return { | ||
results: context.results, | ||
errors: tasks.err.map((err) => err.error), | ||
}; | ||
} | ||
catch (error) { | ||
return { | ||
results: tasks.ctx.results, | ||
errors: [error], | ||
}; | ||
} | ||
}, | ||
}; | ||
} |
{ | ||
"name": "graphql-atg", | ||
"version": "1.0.13", | ||
"version": "1.1.0", | ||
"description": "GraphQL Automated Test Generator (ATG) generates automatic tests for you API by fetching the GraphQL introspection schema and by automatically generating requests for that API", | ||
@@ -55,2 +55,3 @@ "repository": "https://github.com/pelletier197/graphql-atg", | ||
"@types/jest": "^27.0.2", | ||
"@types/jest-when": "^2.7.3", | ||
"@types/lodash": "^4.14.175", | ||
@@ -75,3 +76,5 @@ "@types/micromatch": "^4.0.2", | ||
"jest": "^27.2.5", | ||
"jest-extended": "^1.1.0", | ||
"jest-ts-webcompat-resolver": "^1.0.0", | ||
"jest-when": "^3.4.1", | ||
"npm-run-all": "^4.1.5", | ||
@@ -114,3 +117,4 @@ "omit-deep-lodash": "^1.1.5", | ||
"text" | ||
] | ||
], | ||
"setupFilesAfterEnv": ["jest-extended/all"] | ||
}, | ||
@@ -117,0 +121,0 @@ "config": { |
@@ -18,6 +18,3 @@ import { createClient } from '@lib/infrastructure/graphql/client.js' | ||
) | ||
return await executeQueries(client, allQueries, { | ||
concurrency: 3, | ||
failFast: false, | ||
}) | ||
return await executeQueries(client, allQueries, [], config.runner) | ||
} |
@@ -17,3 +17,3 @@ /* eslint-disable functional/no-this-expression */ | ||
`GraphQL request failed with errors | ||
${details.response.errors.map((error) => ` - ${error.message}`)} | ||
${(details.response?.errors || []).map((error) => ` - ${error.message}`)} | ||
@@ -20,0 +20,0 @@ Query: ${tty ? minify(details.query.query) : prettify(details.query.query)} |
import { GraphQLClient, GraphQLResponse } from '@lib/core/graphql/client.js' | ||
import { GraphQLQuery } from '@lib/core/graphql/query/query.js' | ||
import { newMultiTask } from '@lib/core/task/task.js' | ||
import { newMultiTask, TaskContext } from '@lib/core/task/task.js' | ||
import _ from 'lodash' | ||
import { RunnerConfig } from './config.js' | ||
import { FailedGraphQLRequestError } from './error.js' | ||
import { HookCallback, RunnerHook, RunnerHookContext } from './hooks/hook.js' | ||
export enum QueryExecutionStatus { | ||
SUCCESSFUL = 'SUCCESSFUL', | ||
FAILED = 'FAILED', | ||
SKIPPED = 'SKIPPED', | ||
} | ||
export type QueryExecutionResultDetails = { | ||
/** | ||
* The index of the query in the list of queries to execute | ||
*/ | ||
readonly index: number | ||
/** | ||
* The query that was executed during this test | ||
@@ -14,13 +26,13 @@ */ | ||
/** | ||
* This response received when sending the query | ||
* Wether the task was successful, failed or was skipped | ||
*/ | ||
readonly response: GraphQLResponse<unknown> | ||
readonly status: QueryExecutionStatus | ||
/** | ||
* WetherTaskContext or not the request was successful | ||
* This response received when sending the query | ||
*/ | ||
readonly isSuccessful: boolean | ||
readonly response?: GraphQLResponse<unknown> | ||
/** | ||
* The number of time the request took to complete in ms | ||
*/ | ||
readonly executionTimeMilliseconds: number | ||
readonly executionTimeMilliseconds?: number | ||
} | ||
@@ -45,2 +57,7 @@ | ||
/** | ||
* The number of skipped requests | ||
*/ | ||
readonly skipped: number | ||
/** | ||
* The total execution time for all the test suite. | ||
@@ -59,2 +76,3 @@ */ | ||
queries: ReadonlyArray<GraphQLQuery>, | ||
hooks: ReadonlyArray<RunnerHook>, | ||
config?: Partial<RunnerConfig> | ||
@@ -68,3 +86,4 @@ ): Promise<QueryExecutionResults> { | ||
name: `Query ${index + 1}`, | ||
run: async () => await runQuery(query, client), | ||
run: async (context) => | ||
await runQuery(index, query, client, context, hooks), | ||
} | ||
@@ -79,27 +98,38 @@ }), | ||
try { | ||
const multiTaskResult = await task.start() | ||
const allDetails = [ | ||
...multiTaskResult.results, | ||
...multiTaskResult.errors.map((error) => convertError(error)), | ||
] | ||
return { | ||
resultDetails: allDetails, | ||
failed: allDetails.filter((result) => !result.isSuccessful).length, | ||
successful: allDetails.filter((result) => result.isSuccessful).length, | ||
executionTimeMilliseconds: allDetails.reduce( | ||
(previous: number, current: QueryExecutionResultDetails) => | ||
previous + current.executionTimeMilliseconds, | ||
0 | ||
const multiTaskResult = await task.start() | ||
const results = multiTaskResult.results | ||
const errors = multiTaskResult.errors.map((error) => convertError(error)) | ||
const allExpectedIndexes = queries.map((_, index) => index) | ||
const allReceivedIndexes = [...results, ...errors].map( | ||
(detail) => detail.index | ||
) | ||
const allDetails = _.sortBy( | ||
[ | ||
...results, | ||
...errors, | ||
..._.difference(allExpectedIndexes, allReceivedIndexes).map((index) => | ||
createSkippedResult(index, queries[index]) | ||
), | ||
} | ||
} catch (error) { | ||
const converted = convertError(error) | ||
], | ||
(detail) => detail.index | ||
) | ||
return { | ||
executionTimeMilliseconds: converted.executionTimeMilliseconds, | ||
failed: 1, | ||
successful: 0, | ||
resultDetails: [converted], | ||
} | ||
return { | ||
resultDetails: allDetails, | ||
failed: allDetails.filter( | ||
(result) => result.status === QueryExecutionStatus.FAILED | ||
).length, | ||
successful: allDetails.filter( | ||
(result) => result.status === QueryExecutionStatus.SUCCESSFUL | ||
).length, | ||
skipped: allDetails.filter( | ||
(result) => result.status === QueryExecutionStatus.SKIPPED | ||
).length, | ||
executionTimeMilliseconds: allDetails.reduce( | ||
(previous: number, current: QueryExecutionResultDetails) => | ||
previous + (current.executionTimeMilliseconds || 0), | ||
0 | ||
), | ||
} | ||
@@ -116,12 +146,54 @@ } | ||
function createSkippedResult( | ||
index: number, | ||
query: GraphQLQuery | ||
): QueryExecutionResultDetails { | ||
return { | ||
status: QueryExecutionStatus.SKIPPED, | ||
query, | ||
index, | ||
response: undefined, | ||
executionTimeMilliseconds: undefined, | ||
} | ||
} | ||
async function notifyHooks( | ||
hooks: ReadonlyArray<RunnerHook>, | ||
context: RunnerHookContext, | ||
extractor: (hook: RunnerHook) => HookCallback | undefined | ||
) { | ||
return await Promise.all( | ||
hooks.map(async (hook) => { | ||
const callback = extractor(hook) | ||
if (callback) { | ||
return await callback(context) | ||
} | ||
}) | ||
) | ||
} | ||
async function runQuery( | ||
index: number, | ||
query: GraphQLQuery, | ||
client: GraphQLClient | ||
client: GraphQLClient, | ||
context: TaskContext, | ||
hooks: ReadonlyArray<RunnerHook> | ||
): Promise<QueryExecutionResultDetails> { | ||
const hookContext: RunnerHookContext = { | ||
query: query, | ||
task: context, | ||
} | ||
notifyHooks(hooks, hookContext, (hook) => hook.beforeTest) | ||
const before = new Date().getUTCMilliseconds() | ||
const result = await client.request(query.query, query.variables) | ||
const details = { | ||
const details: QueryExecutionResultDetails = { | ||
index: index, | ||
executionTimeMilliseconds: new Date().getUTCMilliseconds() - before, | ||
isSuccessful: result.errors.length === 0, | ||
status: | ||
result.errors.length === 0 | ||
? QueryExecutionStatus.SUCCESSFUL | ||
: QueryExecutionStatus.FAILED, | ||
query: query, | ||
@@ -131,7 +203,9 @@ response: result, | ||
if (result.errors.length > 0) { | ||
if (details.status !== QueryExecutionStatus.SUCCESSFUL) { | ||
notifyHooks(hooks, hookContext, (hook) => hook.onFail) | ||
throw new FailedGraphQLRequestError(details) | ||
} | ||
notifyHooks(hooks, hookContext, (hook) => hook.onSuccess) | ||
return details | ||
} |
@@ -17,3 +17,3 @@ import getPackageVersion from '@jsbits/get-package-version' | ||
'-e, --endpoint <endpoint>', | ||
'The GraphQL endpoint to test against. ' | ||
'The GraphQL endpoint to test against.' | ||
) | ||
@@ -28,3 +28,3 @@ .option( | ||
'-iid, --introspection.include-deprecated', | ||
'Wether or not the introspection should include the deprecated fields or not', | ||
'Wether or not the introspection should include the deprecated fields or not.', | ||
true | ||
@@ -47,3 +47,3 @@ ) | ||
'-gns, --generation.null-strategy <strategy>', | ||
'Allow specifying if the default behaviour for nullable input values when there is no factory provided is to always use null values, sometimes use null values, or never use null values' | ||
'Allow specifying if the default behaviour for nullable input values when there is no factory provided is to always use null values, sometimes use null values, or never use null values.' | ||
) | ||
@@ -59,3 +59,3 @@ .choices([ | ||
'-rc, --runner.concurrency', | ||
'The number of parallel queries to execute', | ||
'The number of parallel queries to execute.', | ||
validatedParseInt, | ||
@@ -66,3 +66,3 @@ 1 | ||
'-rff, --runner.fail-fast', | ||
'Either the tests should stop after the first error is encountered, or keep running until all queries have been executed', | ||
'Either the tests should stop after the first error is encountered, or keep running until all queries have been executed.', | ||
false | ||
@@ -69,0 +69,0 @@ ) |
@@ -100,6 +100,13 @@ /* eslint-disable functional/immutable-data */ | ||
start: async () => { | ||
const context = await tasks.run() | ||
return { | ||
results: context.results, | ||
errors: tasks.err.map((err) => err.error), | ||
try { | ||
const context = await tasks.run() | ||
return { | ||
results: context.results, | ||
errors: tasks.err.map((err) => err.error), | ||
} | ||
} catch (error) { | ||
return { | ||
results: tasks.ctx.results, | ||
errors: [error as Error], | ||
} | ||
} | ||
@@ -106,0 +113,0 @@ }, |
{ | ||
"extends": "./tsconfig.json", | ||
"exclude": [ | ||
"spec/**/*.spec.ts" | ||
] | ||
} | ||
"extends": "./tsconfig.json", | ||
"exclude": ["spec/**/*.ts"] | ||
} |
@@ -26,3 +26,3 @@ { | ||
"types": ["node", "jest"], | ||
"typeRoots": ["node_modules/@types", "src/types"], | ||
"typeRoots": ["node_modules/@types", "src/types", "spec/types"], | ||
"baseUrl": ".", | ||
@@ -34,5 +34,5 @@ "paths": { | ||
}, | ||
"include": ["src/**/*.ts", "spec/**/*.spec.ts"], | ||
"include": ["src/**/*.ts", "spec/**/*.ts"], | ||
"exclude": ["node_modules/**"], | ||
"compileOnSave": false | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
871139
87
5508
36