Skip to content

Commit 96c075e

Browse files
feat(package.json): add @types/glob as a dev dependency for type definitions
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
1 parent 3224865 commit 96c075e

File tree

6 files changed

+138
-1
lines changed

6 files changed

+138
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"homepage": "https://github.com/multipliedtwice/graphql-query-purifier#readme",
2929
"devDependencies": {
3030
"@types/express": "^4.17.21",
31+
"@types/glob": "^8.1.0",
3132
"@types/jest": "^29.5.11",
3233
"@types/node": "^20.10.6",
3334
"eslint": "^8.56.0",

src/__tests__/merge.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,15 @@ describe('mergeQueries', () => {
290290
);
291291
expect(mergeQueries(requestQuery, allowedQuery)).toBe(expected);
292292
});
293+
294+
test('should handle requestQuery with only whitespace', () => {
295+
const requestQuery = ' ';
296+
const allowedQueries = { user: `{ user { id, name } }` };
297+
const expected = '';
298+
const allowedQuery = getAllowedQueryForRequest(
299+
requestQuery,
300+
allowedQueries
301+
);
302+
expect(mergeQueries(requestQuery, allowedQuery)).toBe(expected);
303+
});
293304
});

src/__tests__/purifier.spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { GraphQLQueryPurifier } from '..';
2+
import { Request, Response, NextFunction } from 'express';
3+
import fs from 'fs';
4+
import glob from 'glob';
5+
6+
jest.mock('fs');
7+
jest.mock('glob');
8+
9+
describe('GraphQLQueryPurifier', () => {
10+
let purifier: GraphQLQueryPurifier;
11+
const gqlPath = './graphql/queries';
12+
const mockReq = {} as Request;
13+
const mockRes = {} as Response;
14+
const mockNext = jest.fn() as NextFunction;
15+
16+
beforeEach(() => {
17+
purifier = new GraphQLQueryPurifier({ gqlPath, debug: true });
18+
mockReq.body = {};
19+
mockRes.status = jest.fn().mockReturnThis();
20+
mockRes.send = jest.fn();
21+
22+
// Mock the necessary functions
23+
(fs.watch as jest.Mock).mockImplementation((path, options, callback) => {
24+
// Simulate file change
25+
callback('change', 'test.gql');
26+
return { close: jest.fn() };
27+
});
28+
29+
(fs.readFileSync as jest.Mock).mockReturnValue(`
30+
query getUser {
31+
user {
32+
id
33+
name
34+
}
35+
}
36+
`);
37+
38+
(glob.sync as jest.Mock).mockReturnValue(['./graphql/queries/test.gql']);
39+
});
40+
41+
afterEach(() => {
42+
jest.clearAllMocks();
43+
});
44+
45+
test('should allow all queries if allowAll is true', () => {
46+
purifier = new GraphQLQueryPurifier({ gqlPath, allowAll: true });
47+
purifier.filter(mockReq, mockRes, mockNext);
48+
expect(mockNext).toHaveBeenCalled();
49+
});
50+
51+
test('should allow Apollo Studio introspection if allowStudio is true', () => {
52+
purifier = new GraphQLQueryPurifier({ gqlPath, allowStudio: true });
53+
mockReq.body = { operationName: 'IntrospectionQuery' };
54+
purifier.filter(mockReq, mockRes, mockNext);
55+
expect(mockNext).toHaveBeenCalled();
56+
});
57+
58+
test('should block Apollo Studio introspection if allowStudio is false', () => {
59+
purifier = new GraphQLQueryPurifier({ gqlPath, allowStudio: false });
60+
mockReq.body = { operationName: 'IntrospectionQuery' };
61+
purifier.filter(mockReq, mockRes, mockNext);
62+
expect(mockRes.status).toHaveBeenCalledWith(403);
63+
expect(mockRes.send).toHaveBeenCalledWith(
64+
'Access to studio is disabled by GraphQLQueryPurifier, pass { allowStudio: true }'
65+
);
66+
});
67+
68+
test('should block queries not in the allowed list', () => {
69+
purifier = new GraphQLQueryPurifier({ gqlPath });
70+
purifier['queryMap'] = {};
71+
mockReq.body = { query: '{ user { id, name, email } }' };
72+
purifier.filter(mockReq, mockRes, mockNext);
73+
expect(mockReq.body.query).toBe('{ __typename }');
74+
expect(mockNext).toHaveBeenCalled();
75+
});
76+
77+
test('should log and block queries that result in empty filtered queries', () => {
78+
purifier = new GraphQLQueryPurifier({ gqlPath, debug: true });
79+
purifier['queryMap'] = {
80+
'getUser.user': '{ user { id } }',
81+
};
82+
mockReq.body = { query: '{ user { name } }' };
83+
purifier.filter(mockReq, mockRes, mockNext);
84+
expect(mockReq.body.query).toBe('{ __typename }');
85+
expect(mockNext).toHaveBeenCalled();
86+
});
87+
88+
test('should handle empty request query', () => {
89+
purifier = new GraphQLQueryPurifier({ gqlPath });
90+
mockReq.body = { query: '' };
91+
purifier.filter(mockReq, mockRes, mockNext);
92+
expect(mockReq.body.query).toBe('');
93+
expect(mockNext).toHaveBeenCalled();
94+
});
95+
96+
test('should handle request query with only whitespace', () => {
97+
purifier = new GraphQLQueryPurifier({ gqlPath });
98+
mockReq.body = { query: ' ' };
99+
purifier.filter(mockReq, mockRes, mockNext);
100+
expect(mockReq.body.query).toBe('{ __typename }');
101+
expect(mockNext).toHaveBeenCalled();
102+
});
103+
});

src/get-allowed-query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export function getAllowedQueryForRequest(
44
requestQuery: string,
55
allowedQueriesMap: { [key: string]: string }
66
): string {
7-
if (!requestQuery) return '';
7+
if (!requestQuery || !requestQuery.trim()) return '';
88
const parsedRequestQuery = parse(requestQuery);
99
const operationDefinition = parsedRequestQuery.definitions.find(
1010
(def) => def.kind === 'OperationDefinition'

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ export class GraphQLQueryPurifier {
8383
*/
8484
private loadQueries() {
8585
const files = glob.sync(`${this.gqlPath}/**/*.gql`.replace(/\\/g, '/'));
86+
if (!files || files.length === 0) {
87+
console.warn(`No GraphQL files found in path: ${this.gqlPath}`);
88+
return;
89+
}
8690
this.queryMap = {};
8791

8892
files.forEach((file: string) => {

yarn.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,16 @@ __metadata:
963963
languageName: node
964964
linkType: hard
965965

966+
"@types/glob@npm:^8.1.0":
967+
version: 8.1.0
968+
resolution: "@types/glob@npm:8.1.0"
969+
dependencies:
970+
"@types/minimatch": ^5.1.2
971+
"@types/node": "*"
972+
checksum: 9101f3a9061e40137190f70626aa0e202369b5ec4012c3fabe6f5d229cce04772db9a94fa5a0eb39655e2e4ad105c38afbb4af56a56c0996a8c7d4fc72350e3d
973+
languageName: node
974+
linkType: hard
975+
966976
"@types/graceful-fs@npm:^4.1.3":
967977
version: 4.1.9
968978
resolution: "@types/graceful-fs@npm:4.1.9"
@@ -1028,6 +1038,13 @@ __metadata:
10281038
languageName: node
10291039
linkType: hard
10301040

1041+
"@types/minimatch@npm:^5.1.2":
1042+
version: 5.1.2
1043+
resolution: "@types/minimatch@npm:5.1.2"
1044+
checksum: 0391a282860c7cb6fe262c12b99564732401bdaa5e395bee9ca323c312c1a0f45efbf34dce974682036e857db59a5c9b1da522f3d6055aeead7097264c8705a8
1045+
languageName: node
1046+
linkType: hard
1047+
10311048
"@types/node@npm:*":
10321049
version: 20.9.2
10331050
resolution: "@types/node@npm:20.9.2"
@@ -2183,6 +2200,7 @@ __metadata:
21832200
resolution: "graphql-query-purifier@workspace:."
21842201
dependencies:
21852202
"@types/express": ^4.17.21
2203+
"@types/glob": ^8.1.0
21862204
"@types/jest": ^29.5.11
21872205
"@types/node": ^20.10.6
21882206
eslint: ^8.56.0

0 commit comments

Comments
 (0)