From 6a559337971090ebe77d0d458cba5878e98de7b6 Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Wed, 25 May 2022 19:35:59 -0400 Subject: [PATCH 1/7] Initial tests for middleware funcitonality. --- package.json | 2 +- src/@types/rateLimit.d.ts | 4 +- src/middleware/index.ts | 4 +- test/middleware/express.test.ts | 341 ++++++++++++++++++++++++++++++++ 4 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 test/middleware/express.test.ts diff --git a/package.json b/package.json index a154acd..78d3c3d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "jest --passWithNoTests", "lint": "eslint src test", - "lint:fix": "eslint --fix src test", + "lint:fix": "eslint --fix src test @types", "prettier": "prettier --write .", "prepare": "husky install" }, diff --git a/src/@types/rateLimit.d.ts b/src/@types/rateLimit.d.ts index 91ec955..302821c 100644 --- a/src/@types/rateLimit.d.ts +++ b/src/@types/rateLimit.d.ts @@ -40,4 +40,6 @@ interface TokenBucketOptions { } // TODO: This will be a union type where we can specify Option types for other Rate Limiters -type RateLimiterOptions = TokenBucketOptions; +// Record represents the empty object for alogorithms that don't require settings +// and might be able to be removed in the future. +type RateLimiterOptions = TokenBucketOptions | Record; diff --git a/src/middleware/index.ts b/src/middleware/index.ts index 57fc4d6..80ba2c3 100644 --- a/src/middleware/index.ts +++ b/src/middleware/index.ts @@ -17,7 +17,8 @@ import { defaultTypeWeightsConfig } from '../analysis/buildTypeWeights'; * Defaults to {mutation: 10, object: 1, field: 0, connection: 2} * @returns {RequestHandler} express middleware that computes the complexity of req.query and calls the next middleware * if the query is allowed or sends a 429 status if the request is blocked - * @throws ValidationError if GraphQL Schema is invalid + * FIXME: How about the specific GraphQLError? + * @throws ValidationError if GraphQL Schema is invalid. */ export function expressRateLimiter( rateLimiter: RateLimiterSelection, @@ -31,7 +32,6 @@ export function expressRateLimiter( // TODO: Connect to Redis store using provided options. Default to localhost:6379 // TODO: Configure the selected RateLimtier // TODO: Configure the complexity analysis algorithm to run for incoming requests - const middleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => { // TODO: Parse query from req.query, compute complexity and pass necessary info to rate limiter // TODO: Call next if query is successful, send 429 status if query blocked, call next(err) with any thrown errors diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts new file mode 100644 index 0000000..e397bf8 --- /dev/null +++ b/test/middleware/express.test.ts @@ -0,0 +1,341 @@ +import { Request, Response, NextFunction, RequestHandler } from 'express'; +import { GraphQLSchema, buildSchema } from 'graphql'; +import redis from 'redis-mock'; +import { RedisClientType } from 'redis'; +import expressRateLimitMiddleware from '../../src/middleware/index'; +import { Socket } from 'net'; + +let middleware: RequestHandler; +let mockRequest: Partial; +let complexRequest: Partial; +let mockResponse: Partial; +const nextFunction: NextFunction = jest.fn(); +const schema: GraphQLSchema = buildSchema(` + type Query { + hero(episode: Episode): Character + reviews(episode: Episode!, first: Int): [Review] + search(text: String): [SearchResult] + character(id: ID!): Character + droid(id: ID!): Droid + human(id: ID!): Human + scalars: Scalars + } + enum Episode { + NEWHOPE + EMPIRE + JEDI + } + interface Character { + id: ID! + name: String! + friends: [Character] + appearsIn: [Episode]! + } + type Human implements Character { + id: ID! + name: String! + homePlanet: String + friends: [Character] + appearsIn: [Episode]! + } + type Droid implements Character { + id: ID! + name: String! + friends: [Character] + primaryFunction: String + appearsIn: [Episode]! + } + type Review { + episode: Episode + stars: Int! + commentary: String + } + union SearchResult = Human | Droid + type Scalars { + num: Int, + id: ID, + float: Float, + bool: Boolean, + string: String + test: Test, + } + type Test { + name: String, + variable: Scalars + } + `); + +describe('Express Middleware tests', () => { + xdescribe('Middleware is configurable...', () => { + describe('...successfully connects to redis using standard connection options', () => { + beforeEach(() => { + // TODO: Setup mock redis store. + }); + + test('...via connection string', () => { + // TODO: use event listener to listen for connections to a mock redis store + expect(true).toBeFalsy(); + }); + + test('via indiividual parameters', () => { + // TODO: + expect(true).toBeFalsy(); + }); + + test('defaults to localhost', () => { + // TODO: + expect(true).toBeFalsy(); + }); + }); + + describe('...Can be configured to use a valid algorithm', () => { + test('... Token Bucket', () => { + // FIXME: Is it possible to check which algorithm was chosen beyond error checking? + expect( + expressRateLimitMiddleware( + 'TOKEN_BUCKET', + { refillRate: 1, bucketSize: 10 }, + schema, + { url: '' } + ) + ).not.toThrow(); + }); + + xtest('...Leaky Bucket', () => { + expect( + expressRateLimitMiddleware( + 'LEAKY_BUCKET', + { refillRate: 1, bucketSize: 10 }, // FIXME: Replace with valid params + schema, + { url: '' } + ) + ).not.toThrow(); + }); + + xtest('...Fixed Window', () => { + expect( + expressRateLimitMiddleware( + 'FIXED_WINDOW', + { refillRate: 1, bucketSize: 10 }, // FIXME: Replace with valid params + schema, + { url: '' } + ) + ).not.toThrow(); + }); + + xtest('...Sliding Window', () => { + expect( + expressRateLimitMiddleware( + 'SLIDING_WINDOW_LOG', + { refillRate: 1, bucketSize: 10 }, // FIXME: Replace with valid params + schema, + { url: '' } + ) + ).not.toThrow(); + }); + + xtest('...Sliding Window Counter', () => { + expect( + expressRateLimitMiddleware( + 'SLIDING_WINDOW_COUNTER', + { refillRate: 1, bucketSize: 10 }, // FIXME: Replace with valid params + schema, + { url: '' } + ) + ).not.toThrow(); + }); + }); + + test('Throw an error for invalid schemas', () => { + const invalidSchema: GraphQLSchema = buildSchema(`{Query {name}`); + + expect( + expressRateLimitMiddleware('TOKEN_BUCKET', {}, invalidSchema, { url: '' }) + ).toThrowError('ValidationError'); + }); + }); + + describe('Middleware is Functional', () => { + // Before each test configure a new middleware amd mock req, res objects. + beforeAll(() => { + jest.useFakeTimers('modern'); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + middleware = expressRateLimitMiddleware( + 'TOKEN_BUCKET', + { refillRate: 1, bucketSize: 10 }, + schema, + {} + ); + mockRequest = { + query: { + // complexity should be 2 (1 Query + 1 Scalar) + query: `Query { + scalars: { + num + } + `, + }, + ip: '123.456', + }; + + mockResponse = { + json: jest.fn(), + send: jest.fn(), + sendStatus: jest.fn(), + locals: {}, + }; + + complexRequest = { + // complexity should be 10 if 'first' is accounted for. + // scalars: 1, droid: 1, reviews (4 * (1 Review, 1 episode)) + query: { + query: `Query { + scalars: { + num + } + droid(id: 1) { + name + } + reviews(episode: 'NEWHOPE', first: 4) { + episode + stars + commentary + } + `, + }, + }; + }); + + xdescribe('Adds expected properties to res.locals', () => { + test('Adds UNIX timestamp and complexity', () => { + const expectedResponse = { + locals: {}, + }; + + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + // We don't actually call json + expect(mockResponse.json).toBeCalledWith(expectedResponse); + expect(mockResponse.locals).toHaveProperty('complexity'); + expect(mockResponse.locals?.complexity).toBeInstanceOf('number'); + expect(mockResponse.locals?.complexity).toBeGreaterThanOrEqual(0); + + expect(mockResponse.locals).toHaveProperty('timestamp'); + expect(mockResponse.locals?.timestamp).toBeInstanceOf('number'); + // confirm that this is timestamp +/- 5 minutes of now. + const now: number = Date.now().valueOf(); + const diff: number = Math.abs(now - (mockResponse.locals?.timestamp || 0)); + expect(diff).toBeLessThan(5 * 60); + }); + }); + + describe('Correctly limits requests', () => { + describe('Allows requests', () => { + test('...a single request', () => { + // successful request calls next without any arguments. + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + expect(nextFunction).toBeCalledTimes(1); + expect(nextFunction).toBeCalledWith(); + }); + + test('Multiple valid requests at > 1 second intervals', () => { + for (let i = 0; i < 3; i++) { + const next: NextFunction = jest.fn(); + middleware(complexRequest as Request, mockResponse as Response, next); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + + // advance the timers by 1 second for the next request + jest.advanceTimersByTime(1000); + } + }); + + test('Multiple valid requests at within one second', () => { + for (let i = 0; i < 3; i++) { + const next: NextFunction = jest.fn(); + middleware(complexRequest as Request, mockResponse as Response, next); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + + // advance the timers by 1 second for the next request + jest.advanceTimersByTime(20); + } + }); + }); + + describe('BLOCKS requests', () => { + test('A single request that exceeds capacity', () => { + complexRequest = { + // complexity should be 12 if 'first' is accounted for. + // scalars: 1, droid: 1, reviews (5 * (1 Review, 1 episode)) + query: { + query: `Query { + scalars: { + num + } + droid(id: 1) { + name + } + reviews(episode: 'NEWHOPE', first: 5) { + episode + stars + commentary + } + `, + }, + }; + const expectedResponse = { + status: 429, + }; + + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + expect(mockResponse.status).toBe(429); + expect(nextFunction).not.toBeCalled(); + + // FIXME: There are multiple functions to send a response + // json, send html, sendStatus etc. How do we check at least one was called + expect(mockResponse.send).toBeCalled(); + }); + + test('Multiple queries that exceed token limit', () => { + for (let i = 0; i < 5; i++) { + // Send 5 queries of complexity 2. These should all succeed + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + // advance the timers by 1 second for the next request + jest.advanceTimersByTime(20); + } + + // Send a 6th request that should be blocked. + const next: NextFunction = jest.fn(); + middleware(mockRequest as Request, mockResponse as Response, next); + expect(mockResponse.status).toBe(429); + expect(next).not.toBeCalled(); + + // FIXME: See above comment on sending responses + expect(mockResponse.send).toBeCalled(); + }); + }); + }); + + test('Uses User IP Address in Redis', async () => { + const client: RedisClientType = redis.createClient(); + // Check for change in the redis store for the IP key + if (!mockRequest.ip) throw new Error('Expected ip to exist on mockRequest'); + const initialValue: string | null = await client.get(mockRequest?.ip); + + middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + const finalValue: string | null = await client.get(mockRequest?.ip); + + expect(finalValue).not.toBeNull(); + expect(finalValue).not.toBe(initialValue); + }); + }); +}); From 16ea57ae33e554d27b7ee658bc3b562238fd3fe1 Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Wed, 25 May 2022 23:43:00 -0400 Subject: [PATCH 2/7] minor test cleanup for express middleware and eslint config change. --- .eslintrc.json | 4 +++- test/middleware/express.test.ts | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 9acdafc..1bdb24e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,7 +19,9 @@ }, "plugins": ["import", "prettier"], "rules": { - + "no-plusplus": [2, { + "allowForLoopAfterthoughts": "true" + }], "prettier/prettier": [ "error" ] diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index e397bf8..0a18b1a 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -220,8 +220,6 @@ describe('Express Middleware tests', () => { middleware(mockRequest as Request, mockResponse as Response, nextFunction); - // We don't actually call json - expect(mockResponse.json).toBeCalledWith(expectedResponse); expect(mockResponse.locals).toHaveProperty('complexity'); expect(mockResponse.locals?.complexity).toBeInstanceOf('number'); expect(mockResponse.locals?.complexity).toBeGreaterThanOrEqual(0); @@ -290,11 +288,9 @@ describe('Express Middleware tests', () => { `, }, }; - const expectedResponse = { - status: 429, - }; middleware(mockRequest as Request, mockResponse as Response, nextFunction); + // FIXME: status is a function. Where does 439 actaully get set. expect(mockResponse.status).toBe(429); expect(nextFunction).not.toBeCalled(); @@ -327,7 +323,6 @@ describe('Express Middleware tests', () => { test('Uses User IP Address in Redis', async () => { const client: RedisClientType = redis.createClient(); // Check for change in the redis store for the IP key - if (!mockRequest.ip) throw new Error('Expected ip to exist on mockRequest'); const initialValue: string | null = await client.get(mockRequest?.ip); middleware(mockRequest as Request, mockResponse as Response, nextFunction); From 4fc2900cb9f356a648cce9f063aa2bd060329656 Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Fri, 27 May 2022 22:09:47 -0400 Subject: [PATCH 3/7] Updated redis tests --- package.json | 1 + test/middleware/express.test.ts | 42 ++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 78d3c3d..1c8a482 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "A GraphQL rate limiting library using query complexity analysis.", "main": "index.js", + "type": "module", "scripts": { "test": "jest --passWithNoTests", "lint": "eslint src test", diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index 0a18b1a..88389b0 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -1,9 +1,9 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'; import { GraphQLSchema, buildSchema } from 'graphql'; -import redis from 'redis-mock'; +import redis from 'redis'; +import redisMock from 'redis-mock'; import { RedisClientType } from 'redis'; import expressRateLimitMiddleware from '../../src/middleware/index'; -import { Socket } from 'net'; let middleware: RequestHandler; let mockRequest: Partial; @@ -70,20 +70,24 @@ describe('Express Middleware tests', () => { describe('...successfully connects to redis using standard connection options', () => { beforeEach(() => { // TODO: Setup mock redis store. + redisMock; }); - test('...via connection string', () => { - // TODO: use event listener to listen for connections to a mock redis store + test('...via url', () => { + // TODO: Connect to redis instance and add 'connect' event listener + // assert that event listener is called once expect(true).toBeFalsy(); }); - test('via indiividual parameters', () => { - // TODO: + test('via socket', () => { + // TODO: Connect to redis instance and add 'connect' event listener + // assert that event listener is called once expect(true).toBeFalsy(); }); test('defaults to localhost', () => { - // TODO: + // TODO: Connect to redis instance and add 'connect' event listener + // assert that event listener is called once expect(true).toBeFalsy(); }); }); @@ -153,6 +157,17 @@ describe('Express Middleware tests', () => { expressRateLimitMiddleware('TOKEN_BUCKET', {}, invalidSchema, { url: '' }) ).toThrowError('ValidationError'); }); + + test('Throw an error in unable to connect to redis', () => { + expect( + expressRateLimitMiddleware( + 'TOKEN_BUCKET', + { bucketSize: 10, refillRate: 1 }, + schema, + { socket: { host: 'localhost', port: 1 } } + ) + ).toThrow('ECONNREFUSED'); + }); }); describe('Middleware is Functional', () => { @@ -321,13 +336,22 @@ describe('Express Middleware tests', () => { }); test('Uses User IP Address in Redis', async () => { + // FIXME: In order to test this accurately the middleware would need to connect + // to a mock instance or the tests would need to connect to an actual redis instance + // We could use NODE_ENV varibale in the implementation to determine the connection type. + + // connecting to the actual redis client here const client: RedisClientType = redis.createClient(); + await client.connect(); // Check for change in the redis store for the IP key - const initialValue: string | null = await client.get(mockRequest?.ip); + + // @ts-ignore mockRequest will always have an ip address. + const initialValue: string | null = await client.get(mockRequest.ip); middleware(mockRequest as Request, mockResponse as Response, nextFunction); - const finalValue: string | null = await client.get(mockRequest?.ip); + // @ts-ignore + const finalValue: string | null = await client.get(mockRequest.ip); expect(finalValue).not.toBeNull(); expect(finalValue).not.toBe(initialValue); From ad8cc4ed6628626467baf6f59f724f4f09be1fdc Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Fri, 27 May 2022 22:16:01 -0400 Subject: [PATCH 4/7] resolved PR comments. --- test/middleware/express.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index 88389b0..071d266 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -208,7 +208,7 @@ describe('Express Middleware tests', () => { complexRequest = { // complexity should be 10 if 'first' is accounted for. - // scalars: 1, droid: 1, reviews (4 * (1 Review, 1 episode)) + // scalars: 1, droid: 1, reviews 8: 1) query: { query: `Query { scalars: { @@ -217,7 +217,7 @@ describe('Express Middleware tests', () => { droid(id: 1) { name } - reviews(episode: 'NEWHOPE', first: 4) { + reviews(episode: 'NEWHOPE', first: 8) { episode stars commentary @@ -319,7 +319,7 @@ describe('Express Middleware tests', () => { // Send 5 queries of complexity 2. These should all succeed middleware(mockRequest as Request, mockResponse as Response, nextFunction); - // advance the timers by 1 second for the next request + // advance the timers by 20 miliseconds for the next request jest.advanceTimersByTime(20); } From 1573f57f9be0b42b7186ef994e9ae24453dec8ec Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Fri, 27 May 2022 22:49:10 -0400 Subject: [PATCH 5/7] updated jest config to use typescript for module support. Corrected middleware tests and set to skip all middleware tests. --- jest.config.js | 7 - jest.config.ts | 11 ++ package-lock.json | 237 ++++++++++++++++++++++++++++++++ package.json | 1 + test/middleware/express.test.ts | 25 ++-- 5 files changed, 262 insertions(+), 19 deletions(-) delete mode 100644 jest.config.js create mode 100644 jest.config.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 81ae109..0000000 --- a/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - roots: ['./test'], - preset: 'ts-jest', - testEnvironment: 'node', - moduleFileExtensions: ['js', 'ts'], -}; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..85af1ef --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,11 @@ +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + verbose: true, + roots: ['./test'], + preset: 'ts-jest', + testEnvironment: 'node', + moduleFileExtensions: ['js', 'ts'], +}; + +export default config; diff --git a/package-lock.json b/package-lock.json index 05bd366..140eab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "prettier": "2.6.2", "redis-mock": "^0.56.3", "ts-jest": "^28.0.2", + "ts-node": "^10.8.0", "typescript": "^4.6.4" } }, @@ -1824,6 +1825,28 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -2427,6 +2450,30 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.1.19", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", @@ -2951,6 +2998,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3044,6 +3100,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3599,6 +3661,12 @@ "semver": "bin/semver.js" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3676,6 +3744,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "28.0.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.0.2.tgz", @@ -7324,6 +7401,49 @@ "node": ">=10" } }, + "node_modules/ts-node": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", + "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -7473,6 +7593,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", @@ -7679,6 +7805,15 @@ "engines": { "node": ">=8" } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } } }, "dependencies": { @@ -8944,6 +9079,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@eslint/eslintrc": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", @@ -9421,6 +9577,30 @@ "@sinonjs/commons": "^1.7.0" } }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, "@types/babel__core": { "version": "7.1.19", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", @@ -9825,6 +10005,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -9889,6 +10075,12 @@ "picomatch": "^2.0.4" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -10311,6 +10503,12 @@ } } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -10365,6 +10563,12 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "diff-sequences": { "version": "28.0.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.0.2.tgz", @@ -13041,6 +13245,27 @@ } } }, + "ts-node": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", + "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -13150,6 +13375,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz", @@ -13312,6 +13543,12 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index 1c8a482..a219b9d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "prettier": "2.6.2", "redis-mock": "^0.56.3", "ts-jest": "^28.0.2", + "ts-node": "^10.8.0", "typescript": "^4.6.4" }, "lint-staged": { diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index 071d266..eba2cfb 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction, RequestHandler } from 'express'; import { GraphQLSchema, buildSchema } from 'graphql'; -import redis from 'redis'; +import * as redis from 'redis'; import redisMock from 'redis-mock'; import { RedisClientType } from 'redis'; import expressRateLimitMiddleware from '../../src/middleware/index'; @@ -9,7 +9,7 @@ let middleware: RequestHandler; let mockRequest: Partial; let complexRequest: Partial; let mockResponse: Partial; -const nextFunction: NextFunction = jest.fn(); +let nextFunction: NextFunction = jest.fn(); const schema: GraphQLSchema = buildSchema(` type Query { hero(episode: Episode): Character @@ -65,8 +65,8 @@ const schema: GraphQLSchema = buildSchema(` } `); -describe('Express Middleware tests', () => { - xdescribe('Middleware is configurable...', () => { +xdescribe('Express Middleware tests', () => { + describe('Middleware is configurable...', () => { describe('...successfully connects to redis using standard connection options', () => { beforeEach(() => { // TODO: Setup mock redis store. @@ -79,13 +79,13 @@ describe('Express Middleware tests', () => { expect(true).toBeFalsy(); }); - test('via socket', () => { + xtest('via socket', () => { // TODO: Connect to redis instance and add 'connect' event listener // assert that event listener is called once expect(true).toBeFalsy(); }); - test('defaults to localhost', () => { + xtest('defaults to localhost', () => { // TODO: Connect to redis instance and add 'connect' event listener // assert that event listener is called once expect(true).toBeFalsy(); @@ -225,9 +225,10 @@ describe('Express Middleware tests', () => { `, }, }; + nextFunction = jest.fn(); }); - xdescribe('Adds expected properties to res.locals', () => { + describe('Adds expected properties to res.locals', () => { test('Adds UNIX timestamp and complexity', () => { const expectedResponse = { locals: {}, @@ -306,7 +307,7 @@ describe('Express Middleware tests', () => { middleware(mockRequest as Request, mockResponse as Response, nextFunction); // FIXME: status is a function. Where does 439 actaully get set. - expect(mockResponse.status).toBe(429); + expect(mockResponse.statusCode).toBe(429); expect(nextFunction).not.toBeCalled(); // FIXME: There are multiple functions to send a response @@ -326,7 +327,7 @@ describe('Express Middleware tests', () => { // Send a 6th request that should be blocked. const next: NextFunction = jest.fn(); middleware(mockRequest as Request, mockResponse as Response, next); - expect(mockResponse.status).toBe(429); + expect(mockResponse.statusCode).toBe(429); expect(next).not.toBeCalled(); // FIXME: See above comment on sending responses @@ -335,13 +336,13 @@ describe('Express Middleware tests', () => { }); }); - test('Uses User IP Address in Redis', async () => { + xtest('Uses User IP Address in Redis', async () => { // FIXME: In order to test this accurately the middleware would need to connect // to a mock instance or the tests would need to connect to an actual redis instance // We could use NODE_ENV varibale in the implementation to determine the connection type. - // connecting to the actual redis client here - const client: RedisClientType = redis.createClient(); + // TODO: connect to the actual redis client here. Make sure to disconnect for proper teardown + const client: RedisClientType = redisMock.createClient(); await client.connect(); // Check for change in the redis store for the IP key From f4c4c6c8c3ae2c38fc08e636bbc2581e27fc487e Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Fri, 27 May 2022 22:56:56 -0400 Subject: [PATCH 6/7] lint fix --- .eslintrc.json | 4 ++-- test/middleware/express.test.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 1bdb24e..bb033d1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,11 +20,11 @@ "plugins": ["import", "prettier"], "rules": { "no-plusplus": [2, { - "allowForLoopAfterthoughts": "true" + "allowForLoopAfterthoughts": true }], "prettier/prettier": [ "error" ] }, - "ignorePatterns": ["jest.config.js"] + "ignorePatterns": ["jest.config.ts"] } diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index eba2cfb..9d8f30f 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -70,7 +70,6 @@ xdescribe('Express Middleware tests', () => { describe('...successfully connects to redis using standard connection options', () => { beforeEach(() => { // TODO: Setup mock redis store. - redisMock; }); test('...via url', () => { @@ -346,11 +345,13 @@ xdescribe('Express Middleware tests', () => { await client.connect(); // Check for change in the redis store for the IP key + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore mockRequest will always have an ip address. const initialValue: string | null = await client.get(mockRequest.ip); middleware(mockRequest as Request, mockResponse as Response, nextFunction); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const finalValue: string | null = await client.get(mockRequest.ip); From 1e67be0bf7d380073a9ab293a122effccea982cc Mon Sep 17 00:00:00 2001 From: Stephan Halarewicz Date: Sat, 28 May 2022 13:38:12 -0400 Subject: [PATCH 7/7] corrected query complexity for express tests --- test/middleware/express.test.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/test/middleware/express.test.ts b/test/middleware/express.test.ts index 9d8f30f..aa75196 100644 --- a/test/middleware/express.test.ts +++ b/test/middleware/express.test.ts @@ -207,20 +207,17 @@ xdescribe('Express Middleware tests', () => { complexRequest = { // complexity should be 10 if 'first' is accounted for. - // scalars: 1, droid: 1, reviews 8: 1) + // Query: 1, droid: 1, reviews 8: 1) query: { query: `Query { - scalars: { - num - } - droid(id: 1) { - name - } - reviews(episode: 'NEWHOPE', first: 8) { - episode - stars - commentary - } + droid(id: 1) { + name + } + reviews(episode: 'NEWHOPE', first: 8) { + episode + stars + commentary + } `, }, }; @@ -284,9 +281,9 @@ xdescribe('Express Middleware tests', () => { describe('BLOCKS requests', () => { test('A single request that exceeds capacity', () => { - complexRequest = { + const blockedRequest: Partial = { // complexity should be 12 if 'first' is accounted for. - // scalars: 1, droid: 1, reviews (5 * (1 Review, 1 episode)) + // scalars: 1, droid: 1, reviews (10 * (1 Review, 0 episode)) query: { query: `Query { scalars: { @@ -295,7 +292,7 @@ xdescribe('Express Middleware tests', () => { droid(id: 1) { name } - reviews(episode: 'NEWHOPE', first: 5) { + reviews(episode: 'NEWHOPE', first: 10) { episode stars commentary @@ -304,8 +301,7 @@ xdescribe('Express Middleware tests', () => { }, }; - middleware(mockRequest as Request, mockResponse as Response, nextFunction); - // FIXME: status is a function. Where does 439 actaully get set. + middleware(blockedRequest as Request, mockResponse as Response, nextFunction); expect(mockResponse.statusCode).toBe(429); expect(nextFunction).not.toBeCalled();