├── .eslintrc.json ├── .github └── pull_request_template.md ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── .vscode └── settings.json ├── @types └── log.d.ts ├── LICENSE ├── README.md ├── jest.unit.json ├── package-lock.json ├── package.json ├── src ├── index.ts └── middleware │ ├── post.ts │ └── verify.ts ├── template.env ├── test ├── index.test.ts ├── middleware │ ├── post.test.ts │ └── verify.test.ts ├── mockConstants.ts └── server.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "airbnb-base", 10 | "airbnb-typescript/base", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/typescript", 13 | "prettier" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": "latest", 18 | "project": "tsconfig.json" 19 | }, 20 | "plugins": ["@typescript-eslint", "prettier"], 21 | "rules": { 22 | "indent": [ 23 | "warn", 24 | 4, 25 | { 26 | "SwitchCase": 2 27 | } 28 | ], 29 | "prettier/prettier": [ 30 | "error" 31 | ] 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | _Provide a short summary of the changes in this PR_ 3 | 4 | ### Type of Change 5 | Please delete options that are not relevant. 6 | 7 | - [ ] Bug fix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | - [ ] This change requires a documentation update 11 | 12 | ### Issues 13 | - Link any issues this PR resolves using keywords (resolve, closes, fixed) 14 | 15 | ### Evidence 16 | - Provide evidence of the the changes functioning as expected or describe your tests. If tests are included in the CI pipeline this may be omitted. 17 | 18 | 19 | _(delete this line)_ Prior to submitting the PR assign a reviewer from each team to review this PR. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # Runs all code quality tools prior to each commit. 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "proseWrap": "never" 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 16 4 | branches: 5 | only: 6 | - dev 7 | script: 8 | - 'npm run lint' 9 | # - "npm run test" 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [], 3 | "settings": {}, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "editor.formatOnSave": true, 8 | } -------------------------------------------------------------------------------- /@types/log.d.ts: -------------------------------------------------------------------------------- 1 | // represents query data stored by limiter into res.locals.graphqlGate 2 | export type LimQueryData = { 3 | // removed depth until functionality is added into limiter 4 | // depth?: number; 5 | complexity?: number; 6 | tokens: number; 7 | success: boolean; 8 | }; 9 | 10 | // represents query data once passed through the logger 11 | export type LogQueryData = { 12 | // removed depth until functionality is added into limiter 13 | // depth?: number; 14 | complexity?: number; 15 | tokens: number; 16 | success: boolean; 17 | timestamp: number; 18 | loggedOn: number; 19 | latency?: number; 20 | }; 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Gate Logger

4 | GitHub stars GitHub issues GitHub last commit 5 | 6 |

The data pipeline package for communication between the GraphQLGate Rate-Limiter and your Gateway developer portal.

7 |
8 | 9 |   10 | 11 | ## Prerequisites 12 | 13 | This package interfaces with the GraphQLGate rate-limiting package to log query data for visualization in the Gateway developer portal 14 | 15 | 1. Signup/login to the [Gateway developer portal](graphqlgate.io). 16 | 17 | 2. Create a new project to recieve a project ID and API key. 18 | 19 | 3. Import and configure the [GraphQLGate rate-limiting package](https://www.npmjs.com/package/graphqlgate) 20 | 21 | ## Getting Started 22 | 23 | Install the package 24 | 25 | ``` 26 | npm i gate-logger 27 | ``` 28 | 29 | Import the package and add the logging middleware to the Express middleware chain BEFORE the GraphQLGate middleware. 30 | 31 | ** ERRORS WILL BE THROWN if the logger is added after the limiter ** 32 | 33 | Copy the project ID and the API key from your project on the Gateway developer portal and include them as middleware arguments. 34 | 35 | ```javascript 36 | // import package 37 | import gateLogger from 'gate-logger'; 38 | import expressGraphQLRateLimiter from 'graphqlgate'; 39 | 40 | /** 41 | * Import other dependencies 42 | * */ 43 | 44 | // Add the logger middleware into your GraphQL middleware chain 45 | app.use('gql', gateLogger(/* PROJECT ID */, /* API KEY */ ); 46 | 47 | //Add the rate limiteing middleware 48 | app.use( 49 | 'gql', 50 | expressGraphQLRateLimiter(schemaObject, { 51 | rateLimiter: { 52 | type: 'TOKEN_BUCKET', 53 | refillRate: 10, 54 | capacity: 100, 55 | }, 56 | }) /** add GraphQL server here */ 57 | ); 58 | ``` 59 | 60 | And that's it! The logger will now send all query rate-limiting data, blocked or allowed, for you to view in the Gateway developer portal! 61 | -------------------------------------------------------------------------------- /jest.unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest", 3 | "coverageDirectory": "./coverage", 4 | "testMatch": ["**/?(*.)+(test).ts"], 5 | "resetMocks": true, 6 | "clearMocks": true 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gate-logger", 3 | "version": "1.0.0", 4 | "description": "A backend library to log GraphQL requests based on their depth and complexity.", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "test": "jest -c ./jest.unit.json", 8 | "test:coverage": "jest --coverage -c ./jest.unit.json", 9 | "lint": "eslint src test", 10 | "lint:fix": "eslint --fix src test", 11 | "prettier": "prettier --write .", 12 | "prepare": "husky install", 13 | "test:server": "nodemon --exec ts-node --files ./test/server.ts" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/oslabs-beta/beaver-logger.git" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/oslabs-beta/beaver-logger/issues" 23 | }, 24 | "homepage": "https://github.com/oslabs-beta/beaver-logger#readme", 25 | "dependencies": { 26 | "axios": "^0.27.2", 27 | "body-parser": "^1.20.0", 28 | "express": "^4.18.1", 29 | "graphql": "^16.5.0", 30 | "mongoose": "^6.3.5" 31 | }, 32 | "devDependencies": { 33 | "@types/express": "^4.17.13", 34 | "@types/jest": "^27.5.2", 35 | "@types/node": "^17.0.34", 36 | "@types/supertest": "^2.0.12", 37 | "@typescript-eslint/eslint-plugin": "^5.23.0", 38 | "@typescript-eslint/parser": "^5.23.0", 39 | "eslint": "^8.15.0", 40 | "eslint-config-airbnb-base": "^15.0.0", 41 | "eslint-config-airbnb-typescript": "^17.0.0", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-plugin-import": "^2.26.0", 44 | "eslint-plugin-prettier": "^4.0.0", 45 | "husky": "^8.0.1", 46 | "jest": "^28.1.0", 47 | "lint-staged": "^12.4.1", 48 | "msw": "^0.42.3", 49 | "prettier": "2.6.2", 50 | "supertest": "^6.2.3", 51 | "ts-jest": "^28.0.3", 52 | "ts-node": "^10.8.0", 53 | "typescript": "^4.6.4" 54 | }, 55 | "lint-staged": { 56 | "*.{js, ts}": "eslint --cache --fix", 57 | "*.{js,ts,css,md}": "prettier --write --ignore-unknown" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | import verification from './middleware/verify'; 4 | import postQuery from './middleware/post'; 5 | 6 | // URI pointing to the visual webapp 7 | const gateURI = 'http://localhost:3000'; 8 | 9 | /** 10 | * @function gatelog runs along with the helper functions 11 | * first, authorizes the user's request to post to the webapp 12 | * using params. This middleware should be called BEFORE the 13 | * rate-limiting middleware in order to log blocked requests & 14 | * allow the limiter to kill requests that must be blocked, 15 | * but it also needs to throw an error if the limiter is not setup. 16 | * 17 | * 18 | * Therefore, this middlware should be placed before the limiter 19 | * e.g. app.use(gatelog(params...)) 20 | * app.use(gatelimiter(params...)) 21 | * 22 | * The gate log adds functionality into a res.on('finish', ()=>{}) 23 | * event listener designed to fire once the response gets sent back to 24 | * the client. This will make sure that the logger has now received data 25 | * from the limiter regarding the current query, and now can send this data 26 | * to the web app's backend. 27 | * 28 | * 29 | * @param projectID points to the user's project in the webapp backend 30 | * 31 | * @param apiKey the authorization code the user provides to post 32 | * to the webapp's backend 33 | * 34 | */ 35 | 36 | // instantation, everything before the return callback runs only once 37 | export default function gateLogger(projectID: string, apiKey: string) { 38 | verification(gateURI, projectID, apiKey) 39 | .then((res) => { 40 | if (res instanceof Error) 41 | throw new SyntaxError( 42 | `[gatelog] Error with server, project ID, or API key:\n${res}` 43 | ); 44 | }) 45 | .catch((err) => { 46 | throw new Error(`[gatelog] Error verifying:\n${err}`); 47 | }); 48 | 49 | // every time a request is processed in the user's backend, 50 | // this express middleware callback will run 51 | return (req: Request, res: Response, next: NextFunction) => { 52 | // calls initial timestamp of request's beginning, 53 | // this will depend on where logger is placed in middleware chain 54 | const timestamp: number = new Date().valueOf(); 55 | 56 | // runs logger's functionality upon response being sent back to client 57 | res.on('finish', async (): Promise => { 58 | // checks if data from limiter middleware contains required properties, 59 | // logs error otherwise to fail without crashing the server 60 | if ( 61 | !( 62 | // prettier-ignore 63 | // eslint-disable-next-line no-prototype-builtins 64 | res.locals.graphqlGate?.hasOwnProperty('success') && 65 | // eslint-disable-next-line no-prototype-builtins 66 | res.locals.graphqlGate?.hasOwnProperty('tokens') 67 | ) 68 | ) { 69 | console.log( 70 | new SyntaxError( 71 | `[gatelog] Error: Limiter is not including required query properties: success & tokens\n` 72 | ) 73 | ); 74 | return; 75 | } 76 | // calls timestamp of request's end 77 | const loggedOn: number = new Date().valueOf(); 78 | 79 | // stores time between request's beginning and end 80 | const latency: number = loggedOn - timestamp; 81 | 82 | const result = await postQuery(gateURI, projectID, { 83 | ...res.locals.graphqlGate, 84 | timestamp, 85 | loggedOn, 86 | latency, 87 | }).catch((err) => { 88 | console.log(new Error(`postQuery.post threw an error: ${err}`)); 89 | }); 90 | 91 | // returns Bad Request code if postQuery fails 92 | if (result instanceof Error) { 93 | console.log(new Error(`postQuery.post threw an error: ${result}`)); 94 | } 95 | }); 96 | 97 | return next(); 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/middleware/post.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { LogQueryData } from '../../@types/log'; 3 | 4 | // returns the result 5 | export default async function postQuery( 6 | gateURI: string, 7 | projectID: string, 8 | queryData: LogQueryData 9 | ): Promise { 10 | // needs depth property once added into limiter functionality 11 | const { complexity, tokens, success, timestamp, loggedOn, latency } = queryData; 12 | 13 | // most likely redundant, in place for strange edge cases 14 | if ( 15 | (complexity && complexity < 0) || 16 | timestamp < 0 || 17 | tokens < 0 || 18 | loggedOn < 0 || 19 | (latency && latency < 0) 20 | ) 21 | throw new SyntaxError(`[gatelog] Query data cannot be negative\n`); 22 | 23 | // returns false to signal index to send back 400 status code 24 | if (!success && complexity) 25 | throw new SyntaxError( 26 | `[gatelog] Complexity should be undefined when query is blocked by limiter\n` 27 | ); 28 | 29 | // TEMP: depth is random until added onto limiter middleware 30 | const depth = Math.round(Math.random() * 10); 31 | 32 | const variables = { 33 | projectQuery: { 34 | projectID, 35 | complexity, 36 | depth, 37 | tokens, 38 | success, 39 | timestamp, 40 | loggedOn, 41 | latency, 42 | }, 43 | }; 44 | 45 | // graphQL query 46 | const query = ` 47 | mutation CreateProjectQuery($projectQuery: CreateProjectQueryInput!) { 48 | createProjectQuery(projectQuery: $projectQuery) { 49 | projectID 50 | complexity 51 | depth 52 | tokens 53 | success 54 | timestamp 55 | loggedOn 56 | latency 57 | } 58 | } 59 | `; 60 | 61 | const data = { 62 | query, 63 | variables, 64 | }; 65 | 66 | // axios post request to the webapp's backend 67 | const result = await axios 68 | .post(`${gateURI}/gql`, data) 69 | .then((json) => json.data.data.createProjectQuery) 70 | .catch((err: Error): Error => { 71 | throw new Error( 72 | `[gatelog] Error posting project query\n${JSON.stringify(err, null, 2)}` 73 | ); 74 | }); 75 | 76 | // check in place to make sure query is posted to the correct project, 77 | // fails without crashing the server 78 | if (result?.projectID !== projectID) { 79 | throw new Error( 80 | `[gatelog] GraphQL error, resulting query's projectID does not match the ID entered\n` 81 | ); 82 | } 83 | 84 | // returns project query object 85 | return result; 86 | } 87 | -------------------------------------------------------------------------------- /src/middleware/verify.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // the current expected length of project IDs 4 | // these are automatically generated by mongoDB 5 | const PROJECT_ID_LENGTH = 24; 6 | 7 | // the current expected length of API keys 8 | // these are random char strings generated in the 9 | // backend of the Gateway webapp 10 | const API_KEY_LENGTH = 10; 11 | 12 | /** 13 | * @function verification validates format of API requests & verifies key provided in header 14 | * matches key in associated project's entry in DB. If either fail, returns error. 15 | * 16 | * @param gateURI the web app's URI, during development it is localhost:3000 17 | * @param projectID the 20 char mongoDB-assigned ID for the project 18 | * @param logKey the 10 char random string given to user from webapp 19 | * 20 | * @returns Error if any of these parameters are syntactically wrong, or if the 21 | * web app's server is having issues, otherwise returns nothing 22 | */ 23 | export default async function verification( 24 | gateURI: string, 25 | projectID: string, 26 | logKey: string 27 | ): Promise { 28 | let dbKey: string | Error = ''; 29 | 30 | // if the gate URI responds with a bad status code, throw an error 31 | // (in place to make sure webapp is running) 32 | await axios(gateURI) 33 | .then((response) => { 34 | if (response.status >= 400) 35 | throw new SyntaxError('[Log API] Invalid Gateway URL provided'); 36 | }) 37 | // throws error if server is not running 38 | .catch((err) => { 39 | throw new Error(`[Log API] Server not running ${err}`); 40 | }); 41 | 42 | // if project query is the wrong length: ?project=[projectID] is required 43 | if (projectID?.length !== PROJECT_ID_LENGTH) { 44 | throw new SyntaxError('[Log API] Project ID passed into middleware is invalid'); 45 | } 46 | 47 | // if provided log_key length is wrong 48 | if (logKey?.length !== API_KEY_LENGTH) { 49 | throw new SyntaxError( 50 | '[Log API] Log_key header is an incorrect length, must be 10 characters.' 51 | ); 52 | } 53 | 54 | // this endpoint returns the associated project's API key 55 | await axios(`${gateURI}/key/${projectID}`) 56 | .then((key: any): void => { 57 | dbKey = key.data; 58 | }) 59 | .catch((err: Error): Error => { 60 | throw new Error(`[Log API] Communication error with Gateway backend ${err}`); 61 | }); 62 | 63 | // if received DB key's length is wrong 64 | if (dbKey.length !== API_KEY_LENGTH) 65 | throw new SyntaxError('[Log API] API key from DB is incorrect length.'); 66 | 67 | if (logKey !== dbKey) 68 | throw new Error( 69 | `[Log API] The log_key provided in header does not match the key of the project specified` 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /template.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Gate-Logger/52ac5380153ca294861a533dd29fc910844bd599/template.env -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import request from 'supertest'; 3 | 4 | import { LimQueryData } from '../@types/log'; 5 | 6 | import app from './server'; 7 | 8 | const MOCK_QUERY_DATA: LimQueryData = { 9 | complexity: Math.round(Math.random() * 10), // query cost 10 | tokens: Math.round(Math.random() * 10), // tokens remaining 11 | success: Math.random() < 0.5, 12 | }; 13 | 14 | /** 15 | * 16 | * TESTING SUITE 17 | * 18 | */ 19 | describe('Logger End to End Test', () => { 20 | describe('successful query', () => { 21 | test('Test API with middleware runs without errors', (done) => { 22 | // merely checks if test API can run the middleware without errors 23 | request(app) 24 | .post('/') 25 | .set('Content-type', 'application/json') 26 | .send({ 27 | mockQueryData: MOCK_QUERY_DATA, 28 | }) 29 | .expect(200) 30 | .expect(MOCK_QUERY_DATA) 31 | .end((err, res) => { 32 | if (err) { 33 | return err; 34 | } 35 | return done(); 36 | }); 37 | }); 38 | }); 39 | 40 | describe('unsuccessful query', () => { 41 | test('negative complexity passed into index', (done) => { 42 | const badMockQueryData: LimQueryData = { 43 | complexity: -1, 44 | tokens: 2, 45 | success: true, 46 | }; 47 | const badData = { 48 | mockQueryData: badMockQueryData, 49 | }; 50 | 51 | request(app) 52 | .post('/') 53 | .set('Content-type', 'application/json') 54 | .send(badData) 55 | .end((err, res) => { 56 | if (err) { 57 | console.log(`Supertest error: ${err}`); 58 | return err; 59 | } 60 | return done(); 61 | }); 62 | }); 63 | 64 | test('query failure and complexity value passed into index', (done) => { 65 | const badMockQueryData: LimQueryData = { 66 | complexity: 2, 67 | tokens: 2, 68 | success: false, 69 | }; 70 | const badData = { 71 | mockQueryData: badMockQueryData, 72 | }; 73 | 74 | request(app) 75 | .post('/') 76 | .set('Content-type', 'application/json') 77 | .send(badData) 78 | .end((err, res) => { 79 | if (err) { 80 | console.log(`Supertest error: ${err}`); 81 | return err; 82 | } 83 | return done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/middleware/post.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import postQuery from '../../src/middleware/post'; 3 | import { LogQueryData } from '../../@types/log'; 4 | 5 | import { MOCK_PROJECT_ID, MOCK_URI } from '../mockConstants'; 6 | 7 | describe('Test API key header verification', () => { 8 | let mockURI: string; 9 | let mockProjectID: string; 10 | let mockQueryData: LogQueryData; 11 | 12 | let newPost: () => Promise; 13 | 14 | beforeEach(() => { 15 | /* The mock data below is pulled from a personal development dB. 16 | 17 | To perform some of these tests yourself, you must connect your dB 18 | to the webapp project and create a mock project with the webapp, 19 | then change these values to match the new data. 20 | */ 21 | mockURI = MOCK_URI; 22 | mockProjectID = MOCK_PROJECT_ID; 23 | mockQueryData = { 24 | complexity: 4, 25 | timestamp: Date.now(), 26 | tokens: 8, 27 | success: true, 28 | loggedOn: 0, 29 | }; 30 | }); 31 | 32 | test('error throws when query data is incorrect', () => { 33 | mockQueryData = { 34 | complexity: 3, 35 | timestamp: 100, 36 | tokens: -1, 37 | success: true, 38 | loggedOn: 0, 39 | }; 40 | 41 | newPost = async () => { 42 | await postQuery(mockURI, mockProjectID, mockQueryData); 43 | }; 44 | 45 | expect(newPost).rejects.toThrowError(); 46 | }); 47 | 48 | test('No error thrown when query data is correct syntax', () => { 49 | newPost = async () => { 50 | await postQuery(mockURI, mockProjectID, mockQueryData); 51 | }; 52 | 53 | expect(newPost).not.toThrow(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/middleware/verify.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | 3 | import verification from '../../src/middleware/verify'; 4 | 5 | import { MOCK_API_KEY, MOCK_PROJECT_ID, MOCK_URI } from '../mockConstants'; 6 | 7 | let newVerification: () => Promise; 8 | 9 | describe('API Verification', () => { 10 | beforeEach(async () => { 11 | newVerification = async () => { 12 | await verification(MOCK_URI, MOCK_PROJECT_ID, MOCK_API_KEY); 13 | }; 14 | }); 15 | /* 16 | SKIP this test when the webapp server is running (it will fail) 17 | */ 18 | xtest('throws when webapp server not running', () => { 19 | expect(newVerification).rejects.toThrow('[Log API] Server not running'); 20 | }); 21 | /* 22 | */ 23 | 24 | test('throws when invalid project ID is provided', () => { 25 | newVerification = async () => { 26 | await verification(MOCK_URI, 'error', MOCK_API_KEY); 27 | }; 28 | 29 | expect(newVerification).rejects.toThrow(); 30 | }); 31 | 32 | test('throws when invalid API key header is provided', () => { 33 | newVerification = async () => { 34 | await verification(MOCK_URI, MOCK_PROJECT_ID, 'error'); 35 | }; 36 | 37 | expect(newVerification).rejects.toThrowError(); 38 | }); 39 | 40 | test('throws when key provided does not match the key in dB', () => { 41 | newVerification = async () => { 42 | await verification(MOCK_URI, MOCK_PROJECT_ID, '10kcharact'); 43 | }; 44 | 45 | expect(newVerification).rejects.toThrow(); 46 | }); 47 | 48 | test('no errors when keys match', () => { 49 | expect(newVerification).not.toThrow(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/mockConstants.ts: -------------------------------------------------------------------------------- 1 | /* The mock data below is pulled from a personal development dB. 2 | 3 | To perform many tests in this suite, you must connect your dB 4 | to the webapp project and create a mock project with the webapp, 5 | then change these values to match the new data. 6 | */ 7 | export const MOCK_PROJECT_ID = '62cce5cf4bb3340a30ad3428'; 8 | 9 | export const MOCK_API_KEY = 'CxVANsQL3o'; 10 | 11 | export const MOCK_URI = 'http://localhost:3000'; 12 | -------------------------------------------------------------------------------- /test/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | 4 | import gatelog from '../src/index'; 5 | 6 | import { MOCK_API_KEY, MOCK_PROJECT_ID } from './mockConstants'; 7 | 8 | const app = express(); 9 | 10 | // logger middleware instantiation, changes res.end definition 11 | app.use(gatelog(MOCK_PROJECT_ID, MOCK_API_KEY)); 12 | 13 | app.use(bodyParser.json()); 14 | 15 | // mocking limiter middleware (to set res.locals.graphQLGate) 16 | // this is in the case that a query goes through successfully 17 | app.use((req, res, next) => { 18 | res.locals.graphqlGate = req.body.mockQueryData; 19 | return next(); 20 | }); 21 | 22 | /** 23 | * this res.end() is in place for ending the middleware chain 24 | * and firing the event listener in gatelog's middleware callback. 25 | * one a response is sent back to the client, the functionality 26 | * within the event listener will execute 27 | */ 28 | app.use('/', (req, res) => { 29 | res.send(req.body.mockQueryData); 30 | }); 31 | 32 | // // for manual middleware tests 33 | // app.listen(3001, () => { 34 | // console.log('test server running'); 35 | // }); 36 | 37 | export default app; 38 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | // repeated from base config's "include" setting 5 | "src", 6 | "test", 7 | 8 | // these are the eslint-only inclusions 9 | ".eslintrc.json" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "removeComments": false, 7 | "preserveConstEnums": true, 8 | "sourceMap": true, 9 | "target": "es5", 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": false, 20 | "typeRoots": ["./@types", "node_modules/@types"], 21 | "types": ["node", "jest"], 22 | "noImplicitAny": true 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.js", "test/**/*.ts", "test/**/*.js", "./@types/*"], 25 | "exclude": ["node_modules", "**/*.spec.ts"] 26 | } 27 | --------------------------------------------------------------------------------