graphql-shield
graphql-shield copied to clipboard
How should I unit test a rule?
Question about GraphQL Shield
I'm unit testing my rules and I managed to do it like this but it doesn't feels right, is very verbose and I have to use "any" on typescript so types don't fail.
it("is valid when a valid token is provided", async () => {
const context = {
req: jest.fn()
}
const isValid = await isAuthenticated.resolve(
null, // parent
null, // args
context, // Context
null, // Info
{} as any, // Options
);
expect(isValid).toEqual(true);
});
Like this resolve does nothing :/
What approach you will use?
- [x ] I have checked other questions and found none that matches mine.
Hey 👋,
I think this is a great question! Would you mind sharing a bit more context regarding the input/output of your tests? I belive we could compose an official function to test rules.
So what I do now is something like this:
export const isAuthenticatedRule = (parent, args, context) => {
return true;
};
export const isAuthenticated = rule({ cache: "contextual" })(
isAuthenticatedRule,
);
So I test isAuthenticatedRule and I pass to the shield isAuthenticated
This looks confusing and messy, do you have a better idea?
Hey 👋,
Sorry for the delay. Thank you for your feedback. Would you mind sharing a bit more "theoretical" background of your tests? I am particularly interested in differences among your tests when you are testing different cache types etc. What is the output/functionality you would expect test to have?
Well, I'm not testing that directly. I have an end to end test that makes the request and then I get back what I expect.
Not sure how you will be able to unit test something like rule({ cache: "contextual" })
I've been fairly interested in graphql-shield and writing specs that is specific to my codebase. So I'll share on how I've been designing my unit tests in order to get confidence in my app without necessarily having to unit test graphql-shield. This library is solid and rather than writing tests in your own library validating the library works as its advertised, all tests related to graphql-shield logic should lie inside this repo. As for in your codebase, you should write specs more geared to the rules you write so that should allow you to feel confident in reusing your rules. I would manually just validate that the caching works completely fine (unless someone has an easy to implement spec :) )
So I will be speaking around the topic of resolver level rules, and not field specific rules. Right now we are currently still developing so I haven't had to face that challenge yet.
Because i'm not doing any field specific rules, I've created helper functions that assert whether the permission passed or did not. (Note: I'm using mocha/chai for my unit tests) The premise of these specs is, if there's anything in the "data" key, then assume it worked and expect no errors. For validating failing tests, assert the exact error. Just to note, the validateSuccessfulRequest expects your the resolvers inputs/payloads to be required in the schema. resolver(input: ResolverInput!): ResolverPayload!
/*
PERMISSION GRANTED SPECS
*/
const validateSuccessfulRequest = (res, operationName, debug = false) => {
if (debug || res.body.errors) {
res.body.should.be.eql({});
// sometimes on errors, the body doesn't get set so we check text as well
res.text.should.be.eql({});
}
res.status.should.be.eql(200);
// since we are doing permission specs, we do assertions on if there is data available and no errors. We don't care
// about what's inside, this is just to check that if it is present, then the user does/does not have access.
res.body.should.not.have.property('errors');
res.body.should.have.property('data');
res.body.should.have.nested.property(`data.${operationName}`).that.should.not.be.null;
};
/*
* PERMISSION DENIED SPECS
* */
export const validateRejectedRequest = (res, operationName, nullable, debug = false) => {
if (debug || res.status === 400) {
// In order to get better visibility of what our error is, we compare it
// to an empty obj to deep display what our payload is
res.body.should.be.eql({});
}
if (nullable) {
res.body.should.have.nested.property(`data.${operationName}`, null);
} else {
res.body.should.have.nested.property(`data`, null);
}
res.body.should.have.property('errors').that.deep.equals([
{
locations: [],
message: 'Not Authorised!',
path: [operationName]
}
]);
};
Then I have reusable spec helpers that are tied to specific rules. The two spec helper functions are tied to specific rules which are named isAdmin and isAuthenticated respectively.
export const adminAllowedSpec = (setupCallback, debug) => {
const specFunc = debug ? it.only : it;
return specFunc(`adminAllowedSpec: allows admin to view the current resource`, async () => {
const { graphqlInstance, operationName, query, variables } = setupCallback();
const admin = await setupAdmin({});
await authorizeUser(admin, graphqlInstance);
const res = await graphqlInstance.send({
query,
variables
});
validateSuccessfulRequest(res, operationName, debug);
});
};
export const isNotAuthenticatedRejectedSpec = (setupCallback, debug) => {
const specFunc = debug ? it.only : it;
return specFunc(`isNotAuthenticatedRejectedSpec: should restrict if not authenticated`, async () => {
const { graphqlInstance, operationName, query, variables, nullable } = setupCallback();
unAuthorizeUser(graphqlInstance);
const res = await graphqlInstance.send({
query,
variables
});
validateRejectedRequest(res, operationName, nullable, debug);
});
};
And then I individually test resolvers independently from one another and I write a describe block related to specifically around permissions
describe('Mutation#myGraphqlResolverMutation', () => {
// vars go here
before(async () => {
//...before setup
});
beforeEach(async () => {
// this is just a request object that points to `/graphql`
graphqlInstance = requestGraphqlInstance();
await authorizeUser(testOwner, graphqlInstance);
setupData = {
graphqlInstance,
operationName: 'myGraphqlResolverMutation',
query: MY_GRAPHQL_RESOLVER_MUTATION,
variables: {
input: {
myInputVariable: 5
}
}
};
});
// there is a 'permissions' block in each of our resolvers forcing us to treat permissions differently than the resolvers logic
describe('permissions', () => {
adminAllowedSpec(() => setupData);
providedUserAllowedToAccessObjectSpec(() => setupData, () => userWhoIsAllowedToAccess);
isNotAuthenticatedRejectedSpec(() => setupData);
});
// the rest of the code the tests the resolver, nothing related to permissions
it('does some cool mutations!', ()=>{
})
});
So far this approach has been splendid. Right now, in the codebase I'm in, we have all the fields whiteliste in order to deny any permissions outright and we have to go field by field in order to think about the proper permissions and enable them. So we've gotten to the point where we think about what rules we need to implement, drop in the helper functions to test specific rules, fail them, and then get them passing once the rule is applied.
We've even gone as far as made a ruleSet helper functions since some specs are specific to the type of argument passed in. These help us group specific mutations/queries based off of the input and reuse all of the same specs for different resolvers.
fruitIdArgPermissionsSpec vegetableIdArgPermissionSpec and each of them import the smaller helper specs, so we can just drop in a function and get a lot of free tests.
Let me know if you guys need me to clean something up in this. Hope someone finds us around this topic. I'm considering even creating an article around the subject but I'd like to get more insight on the topic to see if anyone else has another approach. I'm still tweaking and thinking of new ways.
I've been building a toy graphql-blog and had to set up a little bit of mocking (see applyResolver) to test my permissions. The function is mostly customized function to my use case but I can imagine providing a general one that takes args, variables, and mocks out _shield in context shipped in this package to facilitate testing:
async function applyResolver(
resolver: Rule,
args: any,
variables: any,
context: any,
options: IOptions | null,
) {
context['_shield'] = {
cache: {},
hashFunction
}
if (!options) {
const Options: jest.Mock<IOptions> = jest.fn()
options = new Options()
}
return resolver.resolve({}, args,
context,
variables,
options,
)
}
Or at the very least some documentation to make it easier for others to test. Thoughts on adding this @maticzav?
Note: Rather than testing the individual permissions associated with individual GraphQL queries, I am testing the actual permission resolvers (e.g. isAuthenticated) because this scales better for most apps which have lots of queries but only a small number of permission types. It also orthogonalizes testing concerns. The tests actually helped clear up some corner cases with typescript nulls.
This seems to fail when the rule has { cache: 'strict' } option. I'm still looking for a good solution for this. Any ideas?
@maticzav Have there been any updates on this? Would love to see an official spec for this in the docs.
update: I was able to make use of the func property on the Rule object.
Not sure if this is the recommended approach but it seems to work.
// permissions.js
const { and, or, rule, shield } = require("graphql-shield");
const isAuthenticated = rule()((obj, args, { user }) => user !== null);
exports.isAuthenticated = isAuthenticated;
// permissions.test.js
const { isAuthenticated } = require("../middleware/permissions");
describe("Auth Permissions", () => {
it("should return false if not authenticated", () => {
const ctx = { user: null };
expect(isAuthenticated.func({}, {}, ctx)).toBe(false); // test passes
});
});