diff --git a/.gitignore b/.gitignore index 491fc35..0c57575 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules lib +coverage +.nyc_output +.vscode diff --git a/docs/04-unit-and-integration-tests.md b/docs/04-unit-and-integration-tests.md new file mode 100644 index 0000000..f71cdb0 --- /dev/null +++ b/docs/04-unit-and-integration-tests.md @@ -0,0 +1,8 @@ +## 04 – Unit and Integration Tests + +Let's test our service implementations, endpoint functions, middlewares and all other kinds of code through unit tests. +Also handling integration tests in the sense of doing HTTP requests against the running service. Try it out: + +1. `npm test` – Will run Prettier check, unit tests, and NPM security audit of dependencies +2. `npm run test:unit` – Runs only the unit tests +3. `npm run test:integration` (start server manually before) – Executes the ingration tests on top of the running service diff --git a/package-lock.json b/package-lock.json index 44d9b9d..c118312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,42 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@types/body-parser": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", @@ -14,6 +50,39 @@ "@types/node": "*" } }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/chai-string/-/chai-string-1.4.1.tgz", + "integrity": "sha512-aRNMs6TKgjgPlCHwDfq/YNy5VtRR2hJ4AUWByddrT0TRVVD8eX4MiHW6/iHvmQHRlVuuPZcwnTUE7b4yFt7bEA==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-subset": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.1.tgz", + "integrity": "sha512-Aof+FLfWzBPzDgJ2uuBuPNOBHVx9Siyw4vmOcsMgsuxX1nfUWSlzpq4pdvQiaBgGjGS7vP/Oft5dpJbX4krT1A==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/compression": { "version": "0.0.35", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-0.0.35.tgz", @@ -41,6 +110,12 @@ "@types/express": "*" } }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, "@types/express": { "version": "4.11.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.11.1.tgz", @@ -68,6 +143,12 @@ "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", "dev": true }, + "@types/mocha": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", + "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", + "dev": true + }, "@types/node": { "version": "11.13.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz", @@ -90,6 +171,41 @@ "@types/mime": "*" } }, + "@types/sinon": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.11.tgz", + "integrity": "sha512-6ee09Ugx6GyEr0opUIakmxIWFNmqYPjkqa3/BuxCBokA0klsOLPgMD5K4q40lH7/yZVuJVzOfQpd7pipwjngkQ==", + "dev": true + }, + "@types/sinon-chai": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.2.tgz", + "integrity": "sha512-5zSs2AslzyPZdOsbm2NRtuSNAI2aTWzNKOHa/GRecKo7a5efYD7qGcPxMZXQDayVXT2Vnd5waXxBvV31eCZqiA==", + "dev": true, + "requires": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "@types/superagent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.1.tgz", + "integrity": "sha512-NetXrraTWPcdGG6IwYJhJ5esUGx8AYNiozbc1ENWEsF6BsD4JmNODJczI6Rm1xFPVp6HZESds9YCfqz4zIsM6A==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.4.tgz", + "integrity": "sha512-0TvOJ+6XVMSImgqc2ClNllfVffCxHQhFbsbwOGzGTjdFydoaG052LPqnP8SnmSlnokOcQiPPcbz+Yi30LxWPyA==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -99,17 +215,52 @@ "negotiator": "0.6.1" } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -127,6 +278,22 @@ "type-is": "~1.6.16" } }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -138,6 +305,93 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chai-shallow-deep-equal": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/chai-shallow-deep-equal/-/chai-shallow-deep-equal-1.4.6.tgz", + "integrity": "sha1-QYS1oTOTra40ts7wxptazILE2DQ=", + "dev": true + }, + "chai-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", + "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", + "dev": true + }, + "chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha1-pdDKFOMpp5WW7XAFi2ZGvWmIz+k=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "compressible": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz", @@ -167,6 +421,12 @@ } } }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, "connect-datadog": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/connect-datadog/-/connect-datadog-0.0.6.tgz", @@ -204,6 +464,22 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "data-uri-to-buffer": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.4.tgz", + "integrity": "sha1-RuE6udqOMJdFyNAc5UchPr2y/j8=", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -212,6 +488,20 @@ "ms": "2.0.0" } }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -243,6 +533,12 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -285,6 +581,20 @@ "vary": "~1.1.2" } }, + "expressmocks": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/expressmocks/-/expressmocks-0.1.3.tgz", + "integrity": "sha512-ucSa/ncl/tJ32b8dOhGb9zuCnKaFU5GN8GYCcYmTkGkCJukEZATksV8eFBxhUm6a32YAwFxaeeCz5WOG1s4JzA==", + "dev": true, + "requires": { + "chai": ">=4.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "finalhandler": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", @@ -299,6 +609,21 @@ "unpipe": "~1.0.0" } }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -309,6 +634,65 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, "hot-shots": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-4.8.0.tgz", @@ -338,6 +722,16 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -348,6 +742,30 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lolex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", + "integrity": "sha512-zFo5MgCJ0rZ7gQg69S4pqBsLURbFw11X68C18OcJjJQbqaXm2NoTrGl1IMM3TIz0/BnN1tIs2tzmmqvCsOMMjw==", + "dev": true + }, "make-error": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", @@ -387,6 +805,15 @@ "mime-db": "~1.38.0" } }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -410,6 +837,61 @@ } } }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "mocha-better-spec-reporter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mocha-better-spec-reporter/-/mocha-better-spec-reporter-3.1.0.tgz", + "integrity": "sha1-tGLTkmS5AezyDIyaRpJHRSpXn44=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "data-uri-to-buffer": "0.0.4", + "diff": "^3.1.0", + "graceful-fs": "^4.1.3", + "minimatch": "^3.0.3", + "should-format": "^3.0.2", + "should-type": "^1.4.0", + "source-map": "^0.5.6", + "stack-trace": "0.0.9" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -420,6 +902,36 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-dogstatsd": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/node-dogstatsd/-/node-dogstatsd-0.0.6.tgz", @@ -438,22 +950,48 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "prettier": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz", "integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==", "dev": true }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -484,6 +1022,27 @@ "unpipe": "1.0.0" } }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -530,6 +1089,70 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "sinon": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.2.2.tgz", + "integrity": "sha512-WLagdMHiEsrRmee3jr6IIDntOF4kbI6N2pfbi8wkv50qaUQcBglkzkjtoOEbeJ2vf1EsrHhLI+5Ny8//WHdMoA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.2.0", + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/samsam": "^3.0.2", + "diff": "^3.5.0", + "lolex": "^3.0.0", + "nise": "^1.4.7", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "sinon-chai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.3.0.tgz", + "integrity": "sha512-r2JhDY7gbbmh5z3Q62pNbrjxZdOAjpsqW/8yxAZRSqLZqowmfGZPGUZPFf3UX36NLis0cv8VEM5IJh9HgkSOAA==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -546,11 +1169,84 @@ "source-map": "^0.6.0" } }, + "stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha1-qPbq7KkGdMMz58Q5U/J1tFFRBpU=", + "dev": true + }, "statuses": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "supertest": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.4.2.tgz", + "integrity": "sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA==", + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, "ts-node": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.1.1.tgz", @@ -566,6 +1262,17 @@ "yn": "^2.0.0" } }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", @@ -586,6 +1293,11 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -596,6 +1308,12 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, "yn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", diff --git a/package.json b/package.json index 03aed96..b301b35 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,46 @@ }, "scripts": { "start": "ts-node service/server/index.ts", - "prettier:check": "prettier --check service/**/*.{ts,tsx,js,jsx}", - "prettier:write": "prettier --write service/**/*.{ts,tsx,js,jsx}" + "prettier:check": "prettier --check service/**/*.{ts,tsx,jsx}", + "prettier:write": "prettier --write service/**/*.{ts,tsx,jsx}", + "test": "npm run prettier:check && npm run test:unit && npm audit", + "test:unit": "_mocha 'service/**/*Spec.ts' --opts service/mocha.opts", + "test:integration": "_mocha 'test/integration/**/*Test.ts' --opts service/mocha.opts" }, "dependencies": { "express": "^4.16.2", - "compression": "1.7.2", - "cookie-parser": "1.4.3", - "http-status-codes": "1.3.0", + "compression": "^1.7.2", + "cookie-parser": "^1.4.3", + "http-status-codes": "^1.3.0", "connect-datadog": "^0.0.6", - "hot-shots": "^4.7.0" + "hot-shots": "^4.7.0", + "supertest": "^3.0.0", + "tslib": "^1.9.3" }, "devDependencies": { - "@types/express": "4.11.1", + "@types/chai": "4.1.7", + "@types/chai-as-promised": "7.1.0", + "@types/chai-string": "1.4.1", + "@types/chai-subset": "1.3.1", "@types/compression": "0.0.35", "@types/cookie-parser": "1.4.1", - "typescript": "3.1.1", + "@types/express": "4.11.1", + "@types/supertest": "2.0.4", + "@types/mocha": "5.2.5", + "@types/sinon-chai": "3.2.2", + "chai": "4.2.0", + "chai-as-promised": "7.1.1", + "chai-shallow-deep-equal": "1.4.6", + "chai-string": "1.5.0", + "chai-subset": "1.6.0", + "expressmocks": "^0.1.3", + "mocha": "5.2.0", + "mocha-better-spec-reporter": "3.1.0", + "prettier": "^1.9.2", + "sinon": "7.2.2", + "sinon-chai": "3.3.0", "ts-node": "6.1.1", - "prettier": "^1.9.2" + "typescript": "3.1.1" }, "author": "Jonas Verhoelen ", "repository": { diff --git a/service/mocha.opts b/service/mocha.opts new file mode 100644 index 0000000..4a43b86 --- /dev/null +++ b/service/mocha.opts @@ -0,0 +1,7 @@ +--require ts-node/register +--reporter mocha-better-spec-reporter +--recursive +--require service/setupTestEnvironment.ts +--inspect +--debug-brk +--timeout 5000 diff --git a/service/server/ExpressServer.ts b/service/server/ExpressServer.ts index 275ab02..121c01d 100644 --- a/service/server/ExpressServer.ts +++ b/service/server/ExpressServer.ts @@ -63,7 +63,7 @@ export class ExpressServer { private configureApiEndpoints(server: Express) { server.get('/api/cat', noCache, this.catEndpoints.getAllCats) - server.get('/api/statistics/cat', noCache, this.catEndpoints.getCatStatistics) + server.get('/api/statistics/cat', noCache, this.catEndpoints.getCatsStatistics) server.get('/api/cat/:catId', noCache, this.catEndpoints.getCatDetails) } } diff --git a/service/server/cats/CatEndpoints.ts b/service/server/cats/CatEndpoints.ts index f49e0bd..906a546 100644 --- a/service/server/cats/CatEndpoints.ts +++ b/service/server/cats/CatEndpoints.ts @@ -27,7 +27,7 @@ export class CatEndpoints { } } - public getCatStatistics = async (req: Request, res: Response, next: NextFunction) => { + public getCatsStatistics = async (req: Request, res: Response, next: NextFunction) => { try { res.json(req.services.catService.getCatsStatistics()) } catch (err) { diff --git a/service/server/cats/CatEndpointsSpec.ts b/service/server/cats/CatEndpointsSpec.ts new file mode 100644 index 0000000..2e12866 --- /dev/null +++ b/service/server/cats/CatEndpointsSpec.ts @@ -0,0 +1,93 @@ +import * as sinon from 'sinon' +import { expect } from 'chai' +import * as HttpStatus from 'http-status-codes' +import ExpressMocks, { Mocks } from 'expressmocks' +import { CatEndpoints } from './CatEndpoints' +import { exampleCats } from './exampleCats' + +describe('CatEndpoints', () => { + const sandbox = sinon.createSandbox() + let sampleRequest: any + let catService: any + let endpoints: CatEndpoints + + beforeEach(() => { + endpoints = new CatEndpoints() + catService = { + getCat: sandbox.stub(), + getAllCats: sandbox.stub().returns(exampleCats), + getCatsStatistics: sandbox.stub().returns({ amount: 30, averageAge: 50 }) + } + sampleRequest = { + services: { catService }, + params: { catId: 1 } + } + }) + + describe('getCatDetails', () => { + it('should ask the underlying service for the cat', () => { + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatDetails) + .then(() => expect(catService.getCat).to.have.been.calledWith(1)) + }) + + it('should return the cat as JSON response if it could be found by the service', () => { + catService.getCat.withArgs(1).returns({ id: 1, name: 'Sample Cat' }) + + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatDetails) + .expectJson({ id: 1, name: 'Sample Cat' }) + }) + + it('should send only the 404 status if the cat could not be found', () => { + catService.getCat.withArgs(1).returns(undefined) + + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatDetails) + .expectSendStatus(HttpStatus.NOT_FOUND) + }) + + it('should handle thrown errors by passing them to NextFunction', () => { + const thrownError = new Error('Some problem with accessing the data') + catService.getCat.throws(thrownError) + + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatDetails) + .expectNext(thrownError) + }) + }) + + describe('getAllCats', () => { + it('should return all cats as JSON response', () => { + return ExpressMocks.create(sampleRequest) + .test(endpoints.getAllCats) + .expectJson(exampleCats) + }) + + it('should handle thrown errors by passing them to NextFunction', () => { + const thrownError = new Error('Some problem with accessing the data') + catService.getAllCats.throws(thrownError) + + return ExpressMocks.create(sampleRequest) + .test(endpoints.getAllCats) + .expectNext(thrownError) + }) + }) + + describe('getCatsStatistics', () => { + it('should return the statistics as JSON response', () => { + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatsStatistics) + .expectJson({ amount: 30, averageAge: 50 }) + }) + + it('should handle thrown errors by passing them to NextFunction', () => { + const thrownError = new Error('Some problem with accessing the data') + catService.getCatsStatistics.throws(thrownError) + + return ExpressMocks.create(sampleRequest) + .test(endpoints.getCatsStatistics) + .expectNext(thrownError) + }) + }) +}) diff --git a/service/server/cats/CatRepository.ts b/service/server/cats/CatRepository.ts index 39f3998..0b12e88 100644 --- a/service/server/cats/CatRepository.ts +++ b/service/server/cats/CatRepository.ts @@ -1,4 +1,5 @@ import { Cat } from './Cat' +import { catsById, exampleCats } from './exampleCats' export class CatRepository { public getById(id: number): Cat | undefined { @@ -6,49 +7,6 @@ export class CatRepository { } public getAll(): Cat[] { - return cats + return exampleCats } } - -const cats: Cat[] = [ - { - id: 1, - name: 'Tony Iommi', - breed: 'British Shorthair', - gender: 'male', - age: 71 - }, - { - id: 2, - name: 'Ozzy Osbourne', - breed: 'British Semi-longhair', - gender: 'male', - age: 70 - }, - { - id: 3, - name: 'Geezer Butler', - breed: 'British Longhair', - gender: 'male', - age: 69 - }, - { - id: 4, - name: 'Bill Ward', - breed: 'Burmilla', - gender: 'male', - age: 70 - }, - { - id: 5, - name: 'Sharon Osbourne', - breed: 'Bambino', - gender: 'female', - age: 66 - } -] -type CatsById = { [id: number]: Cat } -const catsById: CatsById = cats.reduce((catzById: CatsById, currentCat) => { - catzById[currentCat.id] = currentCat - return catzById -}, {}) diff --git a/service/server/cats/CatRepositorySpec.ts b/service/server/cats/CatRepositorySpec.ts new file mode 100644 index 0000000..4297993 --- /dev/null +++ b/service/server/cats/CatRepositorySpec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import { CatRepository } from './CatRepository' +import { exampleCats } from './exampleCats' + +describe('CatRepository', () => { + const repository = new CatRepository() + + describe('getById', () => { + it('should find an existing cat in the storage by ID', () => { + const cat = repository.getById(1) + expect(cat).to.deep.equal({ + id: 1, + name: 'Tony Iommi', + breed: 'British Shorthair', + gender: 'male', + age: 71 + }) + }) + + it('should return undefined if a cat is not in storage', () => { + expect(repository.getById(999)).to.be.undefined + }) + }) + + describe('getAll', () => { + it('should find all cats', () => { + expect(repository.getAll()).to.deep.equal(exampleCats) + }) + }) +}) diff --git a/service/server/cats/CatService.ts b/service/server/cats/CatService.ts index 701bc2d..f8ebd26 100644 --- a/service/server/cats/CatService.ts +++ b/service/server/cats/CatService.ts @@ -19,7 +19,7 @@ export class CatService { return { amount: allCats.length, - averageAge: catsAgeSum / allCats.length + averageAge: allCats.length ? catsAgeSum / allCats.length : 0 } } } \ No newline at end of file diff --git a/service/server/cats/CatServiceSpec.ts b/service/server/cats/CatServiceSpec.ts new file mode 100644 index 0000000..6e09629 --- /dev/null +++ b/service/server/cats/CatServiceSpec.ts @@ -0,0 +1,33 @@ +import * as sinon from 'sinon' +import { expect } from 'chai' +import { CatService } from './CatService' +import { exampleCats } from './exampleCats' + +describe('CatService', () => { + const sandbox = sinon.createSandbox() + let catService: CatService + let catRepository: any + + beforeEach(() => { + catRepository = { getAll: sandbox.stub().returns(exampleCats) } + catService = new CatService(catRepository) + }) + + describe('getCatsStatistics', () => { + it('should reflect the total amount of cats', () => { + expect(catService.getCatsStatistics().amount).to.eq(5) + }) + + it('should calculate the average age of all cats', () => { + expect(catService.getCatsStatistics().averageAge).to.eq(69.2) + }) + + it('should calculate an average age of zero if the amount of cats is zero', () => { + catRepository.getAll.returns([]) + expect(catService.getCatsStatistics()).to.deep.equal({ + amount: 0, + averageAge: 0 + }) + }) + }) +}) \ No newline at end of file diff --git a/service/server/cats/exampleCats.ts b/service/server/cats/exampleCats.ts new file mode 100644 index 0000000..9d9def6 --- /dev/null +++ b/service/server/cats/exampleCats.ts @@ -0,0 +1,44 @@ +import { Cat } from './Cat' + +export const exampleCats: Cat[] = [ + { + id: 1, + name: 'Tony Iommi', + breed: 'British Shorthair', + gender: 'male', + age: 71 + }, + { + id: 2, + name: 'Ozzy Osbourne', + breed: 'British Semi-longhair', + gender: 'male', + age: 70 + }, + { + id: 3, + name: 'Geezer Butler', + breed: 'British Longhair', + gender: 'male', + age: 69 + }, + { + id: 4, + name: 'Bill Ward', + breed: 'Burmilla', + gender: 'male', + age: 70 + }, + { + id: 5, + name: 'Sharon Osbourne', + breed: 'Bambino', + gender: 'female', + age: 66 + } +] +type CatsById = { [id: number]: Cat } +export const catsById: CatsById = exampleCats.reduce((catzById: CatsById, currentCat) => { + catzById[currentCat.id] = currentCat + return catzById +}, {}) diff --git a/service/server/middlewares/NoCacheMiddlewareSpec.ts b/service/server/middlewares/NoCacheMiddlewareSpec.ts new file mode 100644 index 0000000..6c5e718 --- /dev/null +++ b/service/server/middlewares/NoCacheMiddlewareSpec.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai' +import * as sinon from 'sinon' +import { noCache } from './NoCacheMiddleware' + +describe('NoCacheMiddleware', () => { + const sandbox = sinon.createSandbox() + let next: sinon.SinonSpy + let requestMock: any + let responseMock: any + + describe('noCache', () => { + beforeEach(() => { + requestMock = {} + responseMock = { + setHeader: sandbox.spy() + } + next = sandbox.spy() + }) + + it('should call "next"', () => { + noCache(requestMock, responseMock, next) + expect(next).to.have.been.calledOnce + }) + + it('should set all required headers', () => { + noCache(requestMock, responseMock, next) + expect(responseMock.setHeader).to.have.been.calledThrice + + sinon.assert.callOrder( + responseMock.setHeader.withArgs('Expires', '0'), + responseMock.setHeader.withArgs('Pragma', 'no-cache'), + responseMock.setHeader.withArgs('Cache-Control', 'no-cache, no-store, must-revalidate') + ) + }) + }) +}) diff --git a/service/server/middlewares/ServiceDependenciesMiddlewareSpec.ts b/service/server/middlewares/ServiceDependenciesMiddlewareSpec.ts new file mode 100644 index 0000000..7a863f6 --- /dev/null +++ b/service/server/middlewares/ServiceDependenciesMiddlewareSpec.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import { RequestHandler } from 'express' +import { addServicesToRequest } from './ServiceDependenciesMiddleware' + +describe('addServicesToRequest', () => { + const services: any = { myService: 'something' } + let middleware: RequestHandler + let requestMock: any + let responseMock: any + + beforeEach(() => { + requestMock = {} + responseMock = {} + middleware = addServicesToRequest(services) + }) + + it('should set "services" on request', done => { + middleware(requestMock, responseMock, () => { + expect(requestMock.services).to.deep.equal(services) + done() + }) + }) + + it('should call next()', done => { + middleware(requestMock, responseMock, error => { + expect(error).to.be.undefined + done() + }) + }) +}) diff --git a/service/setupTestEnvironment.ts b/service/setupTestEnvironment.ts new file mode 100644 index 0000000..903270a --- /dev/null +++ b/service/setupTestEnvironment.ts @@ -0,0 +1,14 @@ +import chai = require('chai') +import chaiString = require('chai-string') +import chaiSubset = require('chai-subset') +import sinon = require('sinon') +import sinonChai = require('sinon-chai') +import chaiAsPromised = require('chai-as-promised') +import chaiShallowDeepEqual = require('chai-shallow-deep-equal') + +chai.should() +chai.use(chaiString) +chai.use(chaiSubset) +chai.use(sinonChai) +chai.use(chaiAsPromised) +chai.use(chaiShallowDeepEqual) diff --git a/test/integration/CatsApiTest.ts b/test/integration/CatsApiTest.ts new file mode 100644 index 0000000..9fa7f29 --- /dev/null +++ b/test/integration/CatsApiTest.ts @@ -0,0 +1,71 @@ +import * as request from 'supertest' +import { expect } from 'chai' +import * as HttpStatus from 'http-status-codes' +import TestEnv from './TestEnv' + +const { baseUrl } = TestEnv + +describe('Cats API', () => { + describe('Cat details', () => { + it('should respond with a positive status code if the cat is known', () => { + return request(baseUrl) + .get('/api/cat/1') + .expect(HttpStatus.OK) + }) + + it('should respond with 404 status code if the cat is not known', () => { + return request(baseUrl) + .get('/api/cat/666') + .expect(HttpStatus.NOT_FOUND) + }) + + it('should respond with cat details data if the cat is known', () => { + return request(baseUrl) + .get('/api/cat/1') + .expect((res: Response) => { + expect(res.body).to.deep.equal({ + id: 1, + name: 'Tony Iommi', + breed: 'British Shorthair', + gender: 'male', + age: 71 + }) + }) + }) + }) + + describe('All cats', () => { + it('should respond with a positive status code', () => { + return request(baseUrl) + .get('/api/cat') + .expect(HttpStatus.OK) + }) + + it('should return a reasonable amount of cats entries', () => { + return request(baseUrl) + .get('/api/cat') + .expect((res: Response) => { + expect((res.body as any).length).to.eq(5) + }) + }) + }) + + describe('Cat statistics', () => { + it('should respond with a positive status code', () => { + return request(baseUrl) + .get('/api/statistics/cat') + .expect(HttpStatus.OK) + }) + + it('should deliver a reasonable statistics response', () => { + return request(baseUrl) + .get('/api/statistics/cat') + .expect((res: Response) => { + expect(res.body).to.deep.eq({ + amount: 5, + averageAge: 69.2 + }) + }) + }) + }) +}) diff --git a/test/integration/TestEnv.ts b/test/integration/TestEnv.ts new file mode 100644 index 0000000..7245d18 --- /dev/null +++ b/test/integration/TestEnv.ts @@ -0,0 +1,4 @@ +export default class TestEnv { + public static readonly local = !process.env.BASE_URL + public static readonly baseUrl = process.env.BASE_URL || 'http://localhost:8000' +}