Getting test coverage of GraphQL resolve function - graphql

I'm using mocha chai and supertest to test a new graphql endpoint set up on our Node/Express server.
I have all the tests running and passing accordingly but when I run the following script:
"test-coverage": "nyc mocha tests/ --recursive",
it is not counting the tests for users resolve function in the code coverage.
My graphql query endpoint looks as shown below:
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: () => ({
users: {
type: new GraphQLList(UserType),
args: {
searchByName: { type: GraphQLString },
queryNearbyUsers: { type: GraphQLBoolean },
skip: { type: GraphQLInt },
limit: { type: GraphQLInt }
},
async resolve(parent, args, req) {
const { searchByName, queryNearbyUsers, skip = 0, limit = 20 } = args
// No search criteria was specified so just return an error
if(!searchByName && !queryNearbyUsers)
throw new Error('NO_SEARCH_CRITERIA_SPECIFIED')
...
}
},
...
})
An example of one of my tests:
it('should throw an error (NO_SEARCH_CRITERIA_SPECIFIED) when no params supplied', function(done) {
request.post('spikeql')
.set({ Authorization: `Bearer ${token}`})
.send({ query: '{ users { _id firstName lastName displayName rpr distanceAway avatar { styles { thumb_square }}}}'})
.expect(200) // TODO: Setup GraphQL to match approproate HTTP res codes
.end((err, res) => {
if(err) return done(err)
let errors = res.body.errors
expect(errors).to.be.an('array')
// Check to make sure error was sent properly
expect(errors[0]).to.have.property('message', 'NO_SEARCH_CRITERIA_SPECIFIED')
expect(errors[0].message).to.be.a('string')
done()
})
})
I perform 3 other tests with different inputs for the GET_USERS query. All of them pass. It just doesn't get tracked in coverage report.
New to graphql and unit/integration testing so any help is appreciated. Can supply additional info if needed.

Related

Which `apollo-server-express` Version Work Best For These Apollo Server Packages?

I’m trying to get apollo-server-lambda or apollo-server-express to work with an executable schema for v3.36.
Here are the packages we use:
apollo-server-express#3.36 or apollo-server-lambda#3+
graphql-constraint-directive#3.0.0
#graphql-tools/schema#7.1.3
I ran multiple regression test to make it work, and it does not seem to hit GraphQL.
Here’s my Apollo server config:
const apolloServer = new ApolloServer({
schema: initializeSchema(),
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground(),
{
didEncounterErrors(errors) {
logger.info(`didEncounterErrors:`)
logger.info(errors)
},
async requestDidStart(requestContext) {
logger.info(`Request started! ${requestContext}`);
return {
async parsingDidStart(requestContext) {
logger.info(`Parsing started! ${requestContext}`);
},
async validationDidStart(requestContext) {
logger.info(`Validation started! ${requestContext}`);
}
}
},
}],
context: async ({ event, context, express }) => {
logger.info(`Loading event... ${JSON.stringify(event)}`)
const newContext = {
headers: event.headers,
functionName: context.functionName,
event,
context,
expressRequest: express.req,
user: {} ?? null,
}
logger.info(`context ${JSON.stringify(newContext)}`)
return newContext
},
dataSources: () => {
logger.info('!initializing datasource')
initializeDbConnection()
return {}
},
...(['staging', 'production', 'demo'].includes(process.env.stage as string)
? { introspection: false, playground: false }
: {}),
})
I was able to log the executable schema inside initializeSchema, but it does not seem to hit the GraphQL Typedef and Resolver after upgrading. It just goes straight to context. So, I'm kinda stumped how to make HTTP request hit the Typedef and Resolvers using makeExecutableSchema()
I just need some advise or a list of table that could help me which version works best with the given apollo-server-<server_version>.

GraphQL mutation "Cannot set headers after they are sent to the client"

I'm implementing graphql login mutation to authenticate user login credential. Mutation verifies the password with bcrypt then sends a cookie to the client, which will render user profile based on whether the cookie is a buyer or owner user).
GraphQL Login Mutation Code:
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
loginUser: {
type: UserType,
args: {
email: { type: GraphQLString },
password: { type: GraphQLString }
},
resolve: function (parent, args, { req, res }) {
User.findOne({ email: args.email }, (err, user) => {
if (user) {
bcrypt.compare(args.password, user.password).then(isMatch => {
if (isMatch) {
if (!user.owner) {
res.cookie('cookie', "buyer", { maxAge: 900000, httpOnly: false, path: '/' });
} else {
res.cookie('cookie', "owner", { maxAge: 900000, httpOnly: false, path: '/' });
}
return res.status(200).json('Successful login');
} else {
console.log('Incorrect password');
}
});
}
});
}
}
}
});
Server.js:
app.use("/graphql",
(req, res) => {
return graphqlHTTP({
schema,
graphiql: true,
context: { req, res },
})(req, res);
});
Error message:
(node:10630) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
[0] at ServerResponse.setHeader (_http_outgoing.js:470:11)
[0] at ServerResponse.header (/Users/xxx/xxx/server/node_modules/express/lib/response.js:771:10)
[0] at ServerResponse.append (/Users/xxx/xxx/server/node_modules/express/lib/response.js:732:15)
[0] at ServerResponse.res.cookie (/Users/xxx/xxx/server/node_modules/express/lib/response.js:857:8)
[0] at bcrypt.compare.then.isMatch (/Users/xxx/xxx/server/schema/schema.js:89:41)
I've done some research on this error, but can't seem to find a relevant answer. The issue seems to lie within response body being executing more than once, thus "cannot set headers after they are sent to the client". Since I'm sending both res.cookie() and res.status(200), how could I fix this problem?
express-graphql already sets the status and sends a response for you -- there's no need to call either res.status or res.json inside your resolver.
GraphQL always returns a status of 200, unless the requested query was invalid, in which case it returns a status of 400. If errors occur while executing the request, they will be included the response (in an errors array separate from the returned data) but the status will still be 200. This is all by design -- see additional discussion here.
Instead of calling res.json, your resolver should return a value of the appropriate type (in this particular case UserType), or a Promise that will resolve to this value.
Additionally, you shouldn't utilize callbacks inside resolvers since they are not compatible with Promises. If the bcrypt library you're using supports using Promises, use the appropriate API. If it doesn't, switch to a library that does (like bcryptjs) or wrap your callback inside a Promise. Ditto for whatever ORM you're using.
In the end, your resolver should look something like this:
resolve: function (parent, args, { req, res }) {
const user = await User.findOne({ email: args.email })
if (user) {
const isMatch = await bcrypt.compare(args.password, user.password)
if (isMatch) {
const cookieValue = user.owner ? 'owner' : 'buyer'
res.cookie('cookie', cookieValue, { maxAge: 900000, httpOnly: false, path: '/' })
return user
}
}
// If you want an error returned in the response, just throw it
throw new Error('Invalid credentials')
}

Send POST request to apollo server with "operationName" and "variables"

I follow this doc https://www.apollographql.com/docs/apollo-server/requests.html#postRequests and try to send a POST request to apollo server.
test code:
it('should get author correctly', () => {
const body = {
query: `
query {
getAuthor($id: Int!) {
name
}
}
`,
// operationName: 'query author',
variables: {
id: 1
}
};
return rp.post(body).then(res => {
expect(res.data.getAuthor.name).to.equal('lin');
});
});
rp.js:
const requestPromise = require('request-promise');
const { PORT } = require('./config');
const GRAPHQL_ENDPOINT = `http://localhost:${PORT}/graphql`;
function rp(options) {
function post(body) {
return requestPromise(GRAPHQL_ENDPOINT, {
method: 'POST',
body,
json: true,
headers: {
'Content-Type': 'application/json'
}
});
}
return {
post
};
}
module.exports = rp;
When I run npm test command, got an error:
graphql test suites
Go to http://localhost:3000/graphiql to run queries!
✓ t0
1) should get author correctly
1 passing (79ms)
1 failing
1) graphql test suites
should get author correctly:
StatusCodeError: 400 - {"errors":[{"message":"Syntax Error: Expected Name, found $","locations":[{"line":3,"column":21}]}]}
at new StatusCodeError (node_modules/request-promise-core/lib/errors.js:32:15)
at Request.plumbing.callback (node_modules/request-promise-core/lib/plumbing.js:104:33)
at Request.RP$callback [as _callback] (node_modules/request-promise-core/lib/plumbing.js:46:31)
at Request.self.callback (node_modules/request/request.js:186:22)
at Request.<anonymous> (node_modules/request/request.js:1163:10)
at IncomingMessage.<anonymous> (node_modules/request/request.js:1085:12)
at endReadableNT (_stream_readable.js:1106:12)
at process._tickCallback (internal/process/next_tick.js:178:19)
The format of your query is not valid. There's actually two things wrong. One, variables are defined at the very top of the operation (next to the query or mutation keyword). And, two, if you define a variable, you have to use it. So your query should look more like this:
query($id: Int!) {
getAuthor(id: $id) {
name
}
}

graphqljs resolver with multiple arguments

I am trying to figure out the best way to write a resolver that filters on multiple arguments. I have the following graphql type
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId}) {
return new Promise((resolve, reject) => {
//Code to query the data store for the user with the given UserId
})
}
}
}
});
The User type has the following fields
Name
UserId
Type
Gender
Now if I want to introduce the ability to filter the user based on the name, then what is the best way to do it. The only way I can think of is to modify the resolver to include the additional args and then based on what is passed in send it to the database. For example
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId, name}) {
return new Promise((resolve, reject) => {
//Check which argument is passed in and then run the query against the datastore
})
}
}
}
});
Isn't there a better way to do this? If I want the user to be able to filter on another attribute then it gets more complicated, and the resolve function is going to get huge and complicated.

GraphQL pass args to sub resolve

I have a relationship between User and Post. This is how I query the User Posts.
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
// I want to get here the args.someBooleanArg
return someLogicToGetUserPosts();
}
}
})
});
The main query is:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return someLogicToGetUsers();
}
}
}
});
The problem is the args in the resolve function of the UserType posts is empty object, how do i pass the args from the main query to sub resolves functions?
When resolving the root query you can use object assign to attach the argument to the user object returned.
Then, on the user type, resolve the argument from the root value (first argument of resolve function).
Example:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return Promise.resolve(someLogicToGetUsers()).then(v => {
return Object.assign({}, v, {
someBooleanArg
});
});
}
}
}
});
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
console.log(parent.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});
You can use the resolver fouth argument, info, to receive the desired variable - from Apollo docs:
Every resolver in a GraphQL.js schema accepts four positional arguments:
fieldName(obj, args, context, info)
{ result }
These arguments have
the following meanings and conventional names:
obj: The object that contains the result returned from the resolver on
the parent field, or, in the case of a top-level Query field, the
rootValue passed from the server configuration. This argument enables
the nested nature of GraphQL queries.
args: An object with the
arguments passed into the field in the query. For example, if the
field was called with author(name: "Ada"), the args object would be: {
"name": "Ada" }.
context: This is an object shared by all resolvers in
a particular query, and is used to contain per-request state,
including authentication information, dataloader instances, and
anything else that should be taken into account when resolving the
query. If you're using Apollo Server, read about how to set the
context in the setup documentation.
info: This argument should only be
used in advanced cases, but it contains information about the
execution state of the query, including the field name, path to the
field from the root, and more. It's only documented in the GraphQL.js
source code.
The info seems to be a very undocumented feature, but I'm using it now with no problems (at least until somebody decide to change it).
Here is the trick:
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }, info) {
// I want to get here the args.someBooleanArg
console.log("BINGO!");
console.log(info.variableValues.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});

Resources