Skip to content

Commit

Permalink
feat(package.json): add @types/glob as a dev dependency for type defi…
Browse files Browse the repository at this point in the history
…nitions

feat(merge.spec.ts): add test case to handle requestQuery with only whitespace
test(purifier.spec.ts): add tests for GraphQLQueryPurifier class methods
fix(get-allowed-query.ts): handle empty or whitespace requestQuery in getAllowedQueryForRequest function
refactor(index.ts): add check for empty files array in loadQueries method
  • Loading branch information
multipliedtwice committed Jul 27, 2024
1 parent 3224865 commit 96c075e
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"homepage": "https://github.com/multipliedtwice/graphql-query-purifier#readme",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.6",
"eslint": "^8.56.0",
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/merge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,15 @@ describe('mergeQueries', () => {
);
expect(mergeQueries(requestQuery, allowedQuery)).toBe(expected);
});

test('should handle requestQuery with only whitespace', () => {
const requestQuery = ' ';
const allowedQueries = { user: `{ user { id, name } }` };
const expected = '';
const allowedQuery = getAllowedQueryForRequest(
requestQuery,
allowedQueries
);
expect(mergeQueries(requestQuery, allowedQuery)).toBe(expected);
});
});
103 changes: 103 additions & 0 deletions src/__tests__/purifier.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { GraphQLQueryPurifier } from '..';
import { Request, Response, NextFunction } from 'express';
import fs from 'fs';
import glob from 'glob';

jest.mock('fs');
jest.mock('glob');

describe('GraphQLQueryPurifier', () => {
let purifier: GraphQLQueryPurifier;
const gqlPath = './graphql/queries';
const mockReq = {} as Request;
const mockRes = {} as Response;
const mockNext = jest.fn() as NextFunction;

beforeEach(() => {
purifier = new GraphQLQueryPurifier({ gqlPath, debug: true });
mockReq.body = {};
mockRes.status = jest.fn().mockReturnThis();
mockRes.send = jest.fn();

// Mock the necessary functions
(fs.watch as jest.Mock).mockImplementation((path, options, callback) => {
// Simulate file change
callback('change', 'test.gql');
return { close: jest.fn() };
});

(fs.readFileSync as jest.Mock).mockReturnValue(`
query getUser {
user {
id
name
}
}
`);

(glob.sync as jest.Mock).mockReturnValue(['./graphql/queries/test.gql']);
});

afterEach(() => {
jest.clearAllMocks();
});

test('should allow all queries if allowAll is true', () => {
purifier = new GraphQLQueryPurifier({ gqlPath, allowAll: true });
purifier.filter(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});

test('should allow Apollo Studio introspection if allowStudio is true', () => {
purifier = new GraphQLQueryPurifier({ gqlPath, allowStudio: true });
mockReq.body = { operationName: 'IntrospectionQuery' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
});

test('should block Apollo Studio introspection if allowStudio is false', () => {
purifier = new GraphQLQueryPurifier({ gqlPath, allowStudio: false });
mockReq.body = { operationName: 'IntrospectionQuery' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockRes.send).toHaveBeenCalledWith(
'Access to studio is disabled by GraphQLQueryPurifier, pass { allowStudio: true }'
);
});

test('should block queries not in the allowed list', () => {
purifier = new GraphQLQueryPurifier({ gqlPath });
purifier['queryMap'] = {};
mockReq.body = { query: '{ user { id, name, email } }' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockReq.body.query).toBe('{ __typename }');
expect(mockNext).toHaveBeenCalled();
});

test('should log and block queries that result in empty filtered queries', () => {
purifier = new GraphQLQueryPurifier({ gqlPath, debug: true });
purifier['queryMap'] = {
'getUser.user': '{ user { id } }',
};
mockReq.body = { query: '{ user { name } }' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockReq.body.query).toBe('{ __typename }');
expect(mockNext).toHaveBeenCalled();
});

test('should handle empty request query', () => {
purifier = new GraphQLQueryPurifier({ gqlPath });
mockReq.body = { query: '' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockReq.body.query).toBe('');
expect(mockNext).toHaveBeenCalled();
});

test('should handle request query with only whitespace', () => {
purifier = new GraphQLQueryPurifier({ gqlPath });
mockReq.body = { query: ' ' };
purifier.filter(mockReq, mockRes, mockNext);
expect(mockReq.body.query).toBe('{ __typename }');
expect(mockNext).toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion src/get-allowed-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function getAllowedQueryForRequest(
requestQuery: string,
allowedQueriesMap: { [key: string]: string }
): string {
if (!requestQuery) return '';
if (!requestQuery || !requestQuery.trim()) return '';
const parsedRequestQuery = parse(requestQuery);
const operationDefinition = parsedRequestQuery.definitions.find(
(def) => def.kind === 'OperationDefinition'
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export class GraphQLQueryPurifier {
*/
private loadQueries() {
const files = glob.sync(`${this.gqlPath}/**/*.gql`.replace(/\\/g, '/'));
if (!files || files.length === 0) {
console.warn(`No GraphQL files found in path: ${this.gqlPath}`);
return;
}
this.queryMap = {};

files.forEach((file: string) => {
Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,16 @@ __metadata:
languageName: node
linkType: hard

"@types/glob@npm:^8.1.0":
version: 8.1.0
resolution: "@types/glob@npm:8.1.0"
dependencies:
"@types/minimatch": ^5.1.2
"@types/node": "*"
checksum: 9101f3a9061e40137190f70626aa0e202369b5ec4012c3fabe6f5d229cce04772db9a94fa5a0eb39655e2e4ad105c38afbb4af56a56c0996a8c7d4fc72350e3d
languageName: node
linkType: hard

"@types/graceful-fs@npm:^4.1.3":
version: 4.1.9
resolution: "@types/graceful-fs@npm:4.1.9"
Expand Down Expand Up @@ -1028,6 +1038,13 @@ __metadata:
languageName: node
linkType: hard

"@types/minimatch@npm:^5.1.2":
version: 5.1.2
resolution: "@types/minimatch@npm:5.1.2"
checksum: 0391a282860c7cb6fe262c12b99564732401bdaa5e395bee9ca323c312c1a0f45efbf34dce974682036e857db59a5c9b1da522f3d6055aeead7097264c8705a8
languageName: node
linkType: hard

"@types/node@npm:*":
version: 20.9.2
resolution: "@types/node@npm:20.9.2"
Expand Down Expand Up @@ -2183,6 +2200,7 @@ __metadata:
resolution: "graphql-query-purifier@workspace:."
dependencies:
"@types/express": ^4.17.21
"@types/glob": ^8.1.0
"@types/jest": ^29.5.11
"@types/node": ^20.10.6
eslint: ^8.56.0
Expand Down

0 comments on commit 96c075e

Please sign in to comment.