diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4e838bb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) janjaali and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 67f34bf..1bb5174 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ SendGrid-Mock serves as a simple server mocking the sendgrid-apis for developmen The SendGrid-Mock server and the UI are both contained in the same docker-image which you can pull from [Docker Hub](https://cloud.docker.com/u/ghashange/repository/docker/ghashange/sendgrid-mock) and start it via: ```shell -docker run -it -p 3000:3000 -e "API_KEY=sendgrid-api-key" ghashange/sendgrid-mock:1.8.4 +docker run -it -p 3000:3000 -e "API_KEY=sendgrid-api-key" ghashange/sendgrid-mock:1.9.0 ``` Some prepared HTTP calls can be found [here](./http-calls). @@ -58,7 +58,7 @@ The UI can be accessed at . SendGrid-Mock also supports SSL using [Let's Encrypt](https://letsencrypt.org/). To enable SSL, run it as follows: ```shell -docker run -it -p 3000:3000 -e "API_KEY=sendgrid-api-key" -e "CERT_DOMAINNAMES=[your-domain-name]" -e "CERT_EMAIL=[your-email-address]" ghashange/sendgrid-mock:1.8.4 +docker run -it -p 3000:3000 -e "API_KEY=sendgrid-api-key" -e "CERT_DOMAINNAMES=[your-domain-name]" -e "CERT_EMAIL=[your-email-address]" ghashange/sendgrid-mock:1.9.0 ``` ## Development @@ -71,7 +71,7 @@ The UI can be accessed at . ### Build -Create docker image with `docker build -t ghashange/sendgrid-mock:1.8.4 .`. +Create docker image with `docker build -t ghashange/sendgrid-mock:1.9.0 .`. ### Release diff --git a/package-lock.json b/package-lock.json index 8c298be..25b83d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "body-parser": "^1.20.1", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "express-json-validator-middleware": "^3.0.1", "express-rate-limit": "^6.7.0", "log4js": "^6.4.0", "react": "^16.7.0", @@ -3293,6 +3294,45 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -3302,6 +3342,11 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -3326,11 +3371,20 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "node_modules/@types/node": { "version": "18.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "dev": true + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" }, "node_modules/@types/prettier": { "version": "2.6.3", @@ -3338,6 +3392,35 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -5506,6 +5589,40 @@ "basic-auth": "^2.0.1" } }, + "node_modules/express-json-validator-middleware": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/express-json-validator-middleware/-/express-json-validator-middleware-3.0.1.tgz", + "integrity": "sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==", + "dependencies": { + "@types/express": "^4.17.3", + "@types/express-serve-static-core": "^4.17.2", + "@types/json-schema": "^7.0.4", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/express-json-validator-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/express-json-validator-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "node_modules/express-rate-limit": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", @@ -5547,8 +5664,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.0.0", @@ -9238,7 +9354,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -9366,6 +9481,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -10229,7 +10352,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -12787,6 +12909,45 @@ "@babel/types": "^7.3.0" } }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -12796,6 +12957,11 @@ "@types/node": "*" } }, + "@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -12820,11 +12986,20 @@ "@types/istanbul-lib-report": "*" } }, + "@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, "@types/node": { "version": "18.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==", - "dev": true + "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" }, "@types/prettier": { "version": "2.6.3", @@ -12832,6 +13007,35 @@ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==", "dev": true }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "requires": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -14497,6 +14701,35 @@ "basic-auth": "^2.0.1" } }, + "express-json-validator-middleware": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/express-json-validator-middleware/-/express-json-validator-middleware-3.0.1.tgz", + "integrity": "sha512-DkqrIwS4O1eCqshuBNG76dBV+BuoXhgsiUjW9Rh3aBerlIY6fwIrNQ+UZLqh6CLbqcQRQTbqiNA5AlneqlUflA==", + "requires": { + "@types/express": "^4.17.3", + "@types/express-serve-static-core": "^4.17.2", + "@types/json-schema": "^7.0.4", + "ajv": "^8.11.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "express-rate-limit": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", @@ -14506,8 +14739,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -17284,8 +17516,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.11.0", @@ -17383,6 +17614,11 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -18061,7 +18297,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 1dd9d66..e5bbfd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sendgrid-mock", - "version": "1.8.4", + "version": "1.9.0", "description": "SendGrid API mock", "engines": { "node": ">=16", @@ -31,6 +31,7 @@ "body-parser": "^1.20.1", "express": "^4.18.2", "express-basic-auth": "^1.2.0", + "express-json-validator-middleware": "^3.0.1", "express-rate-limit": "^6.7.0", "log4js": "^6.4.0", "react": "^16.7.0", diff --git a/src/server/ExpressApp.js b/src/server/ExpressApp.js index 4fab2ee..777b215 100644 --- a/src/server/ExpressApp.js +++ b/src/server/ExpressApp.js @@ -1,10 +1,9 @@ -const crypto = require('crypto'); const path = require('path'); const express = require('express'); const basicAuth = require('express-basic-auth'); -const bodyParser = require('body-parser'); -const { loggerFactory } = require('./logger/log4js'); const { rateLimit } = require('express-rate-limit'); +const { loggerFactory } = require('./logger/log4js'); +const RequestHandler = require('./RequestHandler'); const logger = loggerFactory('ExpressApp'); @@ -41,60 +40,10 @@ const setupExpressApp = ( logger.warn('Rate limit is disabled!'); } - app.use(bodyParser.json({ limit: '5mb' })); - - app.post('/v3/mail/send', (req, res) => { - - const reqApiKey = req.headers.authorization; - - if (reqApiKey === `Bearer ${mockedApiAuthenticationKey}`) { - - mailHandler.addMail(req.body); - - res.status(202).header({ - 'X-Message-ID': crypto.randomUUID(), - }).send(); - } else { - res.status(403).send({ - errors: [{ - message: 'Failed authentication', - field: 'authorization', - help: 'check used api-key for authentication', - }], - id: 'forbidden', - }); - } - }); - - app.get('/api/mails', (req, res) => { - - const filterCriteria = { - to: req.query.to, - subject: req.query.subject, - dateTimeSince: req.query.dateTimeSince, - }; - - const paginationCriteria = { - page: req.query.page, - pageSize: req.query.pageSize, - }; - - const mails = mailHandler.getMails(filterCriteria, paginationCriteria); - - res.send(mails); - }); - - app.delete('/api/mails', (req, res) => { + // Request handler for non-static requests. + RequestHandler(app, mockedApiAuthenticationKey, mailHandler); - const filterCriteria = { - to: req.query.to, - }; - - mailHandler.clear(filterCriteria); - - res.sendStatus(202); - }); - + // Static content. app.use(express.static(path.join(__dirname, '../../dist'))); app.get('/', function (req, res) { res.sendFile(path.join(__dirname, '../../dist', 'index.html')); diff --git a/src/server/RequestHandler.js b/src/server/RequestHandler.js new file mode 100644 index 0000000..a46eaf9 --- /dev/null +++ b/src/server/RequestHandler.js @@ -0,0 +1,156 @@ +const bodyParser = require('body-parser'); +const crypto = require('crypto'); +const {Validator, ValidationError} = require('express-json-validator-middleware'); + +const jsonSchema = { + v3MailSend: { + type: 'object', + required: ['personalizations', 'content', 'from', 'subject'], + properties: { + content: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + }, + value: { + type: 'string', + }, + }, + } + }, + from: { + type: 'object', + required: ['email'], + properties: { + email: { + type: 'string', + }, + name: { + type: 'string', + }, + } + }, + personalizations: { + type: 'array', + items: { + type: 'object', + properties: { + to: { + type: 'array', + items: { + type: 'object', + properties: { + email: { + type: 'string', + }, + }, + }, + } + }, + } + }, + subject: { + type: 'string', + }, + } + } +}; + +/** + * Creates a request handler for the given express app. + * + * @param {*} app express app + * @param {*} apiAuthenticationKey api key for authentication + * @param {*} mailHandler mail handler + */ +const RequestHandler = (app, apiAuthenticationKey, mailHandler) => { + + const { validate } = new Validator(); + + // Using application/json parser for parsing the request body with a slightly + // increased limit for the request body size thus allowing larger mails. + app.use(bodyParser.json({ limit: '5mb' })); + + app.post( + '/v3/mail/send', + validate({ body: jsonSchema.v3MailSend }), + (req, res) => { + + const reqApiKey = req.headers.authorization; + + if (reqApiKey === `Bearer ${apiAuthenticationKey}`) { + + mailHandler.addMail(req.body); + + res.status(202).header({ + 'X-Message-ID': crypto.randomUUID(), + }).send(); + } else { + res.status(403).send({ + errors: [{ + message: 'Failed authentication', + field: 'authorization', + help: 'check used api-key for authentication', + }], + id: 'forbidden', + }); + } + } + ); + + app.get('/api/mails', (req, res) => { + + const filterCriteria = { + to: req.query.to, + subject: req.query.subject, + dateTimeSince: req.query.dateTimeSince, + }; + + const paginationCriteria = { + page: req.query.page, + pageSize: req.query.pageSize, + }; + + const mails = mailHandler.getMails(filterCriteria, paginationCriteria); + + res.send(mails); + }); + + app.delete('/api/mails', (req, res) => { + + const filterCriteria = { + to: req.query.to, + }; + + mailHandler.clear(filterCriteria); + + res.sendStatus(202); + }); + + // Error handler that returns a 400 status code if the request body does not + // match the given json schema. + app.use((error, request, response, next) => { + if (error instanceof ValidationError) { + const responseBody = { + errors: error.validationErrors.body.map(validationError => { + return { + field: validationError.params.missingProperty + ? validationError.params.missingProperty + : validationError.propertyName, + message: validationError.message, + path: validationError.schemaPath, + }; + }), + }; + response.status(400).send(responseBody); + next(); + } else { + next(error); + } + }); +}; + +module.exports = RequestHandler; diff --git a/test/server/ExpressApp.test.js b/test/server/ExpressApp.test.js index ee5367e..f43ad01 100644 --- a/test/server/ExpressApp.test.js +++ b/test/server/ExpressApp.test.js @@ -6,19 +6,39 @@ const MailHandler = require('../../src/server/handler/MailHandler'); const rateLimitConfiguration = {enabled: false}; +const testMail = { + 'personalizations': [ + { + 'to': [{ + 'email': 'to@example.com' + }, { + 'email': 'to2@example.com' + }] + } + ], + 'from': { + 'email': 'from@example.com' + }, + 'subject': 'important subject', + 'content': [ + { + 'type': 'text/html', + 'value': 'even more important content containing a link http://example.com/path?query=value' + } + ] +}; + describe('App', () => { describe('POST /v3/mail/send', () => { test('adds mails', async () => { - const mail = {mail: 'important'}; - const mailHandlerStub = sinon.createStubInstance(MailHandler); mailHandlerStub .addMail .withArgs( - sinon.match(mail) + sinon.match(testMail) ) .returns(undefined); @@ -26,7 +46,7 @@ describe('App', () => { const response = await request(sut) .post('/v3/mail/send') - .send(mail) + .send(testMail) .set('Authorization', 'Bearer sonic'); expect(response.statusCode).toBe(202); @@ -34,13 +54,11 @@ describe('App', () => { test('responds with x-message-id header', async () => { - const mail = {mail: 'important'}; - const mailHandlerStub = sinon.createStubInstance(MailHandler); mailHandlerStub .addMail .withArgs( - sinon.match(mail) + sinon.match(testMail) ) .returns(undefined); @@ -48,7 +66,7 @@ describe('App', () => { const response = await request(sut) .post('/v3/mail/send') - .send(mail) + .send(testMail) .set('Authorization', 'Bearer sonic'); expect(response.headers['x-message-id']).toBeDefined(); @@ -60,7 +78,7 @@ describe('App', () => { const sut = setupExpressApp(mailHandlerStub, {enabled: false}, 'sonic', rateLimitConfiguration); - const response = await request(sut).post('/v3/mail/send'); + const response = await request(sut).post('/v3/mail/send').send(testMail); expect(response.statusCode).toBe(403); }); @@ -72,6 +90,7 @@ describe('App', () => { const response = await request(sut) .post('/v3/mail/send') + .send(testMail) .set('Authorization', 'Bearer sonic'); expect(response.statusCode).toBe(202); diff --git a/version b/version index 7b378be..abb1658 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.8.4 \ No newline at end of file +1.9.0 \ No newline at end of file