diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 020fb6e1..d4d42058 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,13 +14,7 @@ jobs: - name: Install modules run: npm install - name: Build the docker-compose stack - run: docker-compose -f docker-compose.testing.yaml up -d - # - name: Run contract tests - # run: npm run test:contract - # env: - # ENVIRONMENT: CI - # JWT_TOKEN: 123 - # JWT_TOKEN_LOGIN: 456 + run: docker compose -f docker-compose.testing.yaml up -d - name: Run integration tests run: npm run test:integration env: diff --git a/Dockerfile b/Dockerfile index 3822f2ed..06ae9810 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.18-alpine3.17 as builder +FROM node:20.8.0-alpine3.17 as builder RUN apk --update add git build-base @@ -16,7 +16,7 @@ COPY /src ./src/ RUN npm run build -FROM node:18.18-alpine3.17 +FROM node:20.8.0-alpine3.17 ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.2/wait /wait RUN chmod +x /wait diff --git a/migrations/1709293662942_notification-type.js b/migrations/1709293662942_notification-type.js new file mode 100644 index 00000000..61236b54 --- /dev/null +++ b/migrations/1709293662942_notification-type.js @@ -0,0 +1,11 @@ +exports.up = (pgm) => { + pgm.createType("notification_type", ["report_detail", "degradation"] ) + pgm.addColumn({ schema: "jtl", name: "notifications" }, { + notification_type: { + type: "notification_type", + "default": "report_detail", + notNull: true, + }, + }) + pgm.renameColumn({ schema: "jtl", name: "notifications" }, "type", "channel") +} diff --git a/package-lock.json b/package-lock.json index 73fdc6b0..8cbf9554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,14 @@ "license": "ISC", "dependencies": { "@breejs/ts-worker": "^2.0.0", - "axios": "^1.6.7", + "axios": "^1.7.4", "bcrypt": "^5.1.1", "body-parser": "^1.20.1", "boom": "^7.2.0", - "bree": "^9.2.2", + "bree": "^9.2.4", "compression": "^1.7.4", - "dotenv": "^16.4.2", - "express": "^4.18.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", "express-winston": "^4.2.0", "fast-csv": "^4.3.6", "helmet": "^6.2.0", @@ -28,10 +28,10 @@ "moment": "^2.30.1", "multer": "^1.4.5-lts.1", "node-pg-migrate": "^6.2.2", - "pg": "^8.11.3", + "pg": "^8.12.0", "pg-promise": "^10.15.4", "uuid": "^9.0.1", - "winston": "^3.11.0", + "winston": "^3.13.1", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz" }, "devDependencies": { @@ -597,9 +597,9 @@ "dev": true }, "node_modules/@breejs/later": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@breejs/later/-/later-4.1.0.tgz", - "integrity": "sha512-QgGnZ9b7o4k0Ai1ZbTJWwZpZcFK9d+Gb+DyNt4UT9x6IEIs5HVu0iIlmgzGqN+t9MoJSpSPo9S/Mm51UtHr3JA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@breejs/later/-/later-4.2.0.tgz", + "integrity": "sha512-EVMD0SgJtOuFeg0lAVbCwa+qeTKILb87jqvLyUtQswGD9+ce2nB52Y5zbTF1Hc0MDFfbydcMcxb47jSdhikVHA==", "engines": { "node": ">= 10" } @@ -621,9 +621,9 @@ } }, "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "engines": { "node": ">=0.1.90" } @@ -2820,6 +2820,11 @@ "@types/serve-static": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "node_modules/@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -3304,11 +3309,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.4", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -3512,12 +3518,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -3525,7 +3531,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3583,11 +3589,12 @@ } }, "node_modules/bree": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/bree/-/bree-9.2.2.tgz", - "integrity": "sha512-KtLjikQwi4RJLMpMj1QEviwh+1UDFIK1ejVa1AiWtAtA0NL7kvLsCpaTxCYTNEx40Q7IA3JWP7utVoGGJh11/w==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/bree/-/bree-9.2.4.tgz", + "integrity": "sha512-3GDVYbRYxPIIKgqu00FlIDD//q/0XkMC+zq74sp/qRRQQUWdc39lsFkdHW2g2lTlhaxbqkHd97p8oRMm/YeSJw==", + "license": "MIT", "dependencies": { - "@breejs/later": "^4.1.0", + "@breejs/later": "^4.2.0", "boolean": "^3.2.0", "combine-errors": "^3.0.3", "cron-validate": "^1.4.5", @@ -3998,9 +4005,9 @@ ] }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -4012,9 +4019,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -4209,9 +4216,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.2.tgz", - "integrity": "sha512-rZSSFxke7d9nYQ5NeMIwp5PP+f8wXgKNljpOb7KtH6SKW1cEqcXAz9VSJYVLKe7Jhup/gUYOkaeSVyK8GJ+nBg==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, @@ -4759,16 +4766,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -5033,9 +5040,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -8794,15 +8801,19 @@ } }, "node_modules/logform": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.4.2.tgz", - "integrity": "sha512-W4c9himeAwXEdZ05dQNerhFz2XG80P9Oj0loPUMV23VC2it0orMHQhJm4hdnnor3rd1HsGf6a2lPwBM1zeXHGw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", "dependencies": { - "@colors/colors": "1.5.0", + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" } }, "node_modules/lru-cache": { @@ -9445,15 +9456,14 @@ } }, "node_modules/pg": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", - "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", - "dependencies": { - "buffer-writer": "2.0.0", - "packet-reader": "1.0.0", - "pg-connection-string": "^2.6.2", - "pg-pool": "^3.6.1", - "pg-protocol": "^1.6.0", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -9479,9 +9489,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -9500,9 +9510,9 @@ } }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", "peerDependencies": { "pg": ">=8.0" } @@ -9547,9 +9557,9 @@ } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -9820,9 +9830,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -9987,9 +9997,9 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-stable-stringify": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.1.tgz", - "integrity": "sha512-dVHE6bMtS/bnL2mwualjc6IxEv1F+OCUpA46pKUj6F8uDbUM0jCCulPqRNPSnWwGNKx5etqMjZYdXtrm5KJZGA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", "engines": { "node": ">=10" } @@ -10492,9 +10502,12 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } }, "node_modules/ts-jest": { "version": "29.0.3", @@ -10835,43 +10848,44 @@ } }, "node_modules/winston": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", - "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.1.tgz", + "integrity": "sha512-SvZit7VFNvXRzbqGHsv5KSmgbEYR5EiQfDAL9gxYkRqa934Hnk++zze0wANKtMHcy/gI4W/3xmSDwlhf865WGw==", + "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.4.0", + "logform": "^2.6.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" + "winston-transport": "^4.7.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", - "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" }, "engines": { - "node": ">= 6.4.0" + "node": ">= 12.0.0" } }, "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10881,14 +10895,6 @@ "node": ">= 6" } }, - "node_modules/winston/node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/winston/node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", diff --git a/package.json b/package.json index 86c44d57..bec0cbff 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,14 @@ "license": "ISC", "dependencies": { "@breejs/ts-worker": "^2.0.0", - "axios": "^1.6.7", + "axios": "^1.7.4", "bcrypt": "^5.1.1", "body-parser": "^1.20.1", "boom": "^7.2.0", - "bree": "^9.2.2", + "bree": "^9.2.4", "compression": "^1.7.4", - "dotenv": "^16.4.2", - "express": "^4.18.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", "express-winston": "^4.2.0", "fast-csv": "^4.3.6", "helmet": "^6.2.0", @@ -37,10 +37,10 @@ "moment": "^2.30.1", "multer": "^1.4.5-lts.1", "node-pg-migrate": "^6.2.2", - "pg": "^8.11.3", + "pg": "^8.12.0", "pg-promise": "^10.15.4", "uuid": "^9.0.1", - "winston": "^3.11.0", + "winston": "^3.13.1", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz" }, "devDependencies": { diff --git a/src/app.ts b/src/app.ts index 345e329d..0534fb14 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,7 @@ import { NextFunction, Request, Response } from "express" import { PgError } from "./server/errors/pgError" import { bree } from "./server/utils/scheduled-tasks/scheduler" import helmet from "helmet" +import { AnalyticsEvent } from "./server/utils/analytics/anyltics-event" const DEFAULT_PORT = 5000 const PORT = process.env.PORT || DEFAULT_PORT @@ -63,6 +64,7 @@ export class App { } const errorId = uuidv4() logger.error(`Unexpected error: ${error}, errorId: ${errorId}`) + AnalyticsEvent.reportUnexpectedError(error) return res.status(StatusCode.InternalError).json({ message: `Unexpected error occurred: ${errorId}` }) }) diff --git a/src/server/controllers/auth/init-user-controller.spec.ts b/src/server/controllers/auth/init-user-controller.spec.ts index fa9b293e..3f992a5d 100644 --- a/src/server/controllers/auth/init-user-controller.spec.ts +++ b/src/server/controllers/auth/init-user-controller.spec.ts @@ -6,37 +6,50 @@ import { AllowedRoles } from "../../middleware/authorization-middleware" jest.mock("../../../db/db") const mockResponse = () => { - const res: Partial = {} - res.send = jest.fn().mockReturnValue(res) - res.status = jest.fn().mockReturnValue(res) - return res + const res: Partial = {} + res.send = jest.fn().mockReturnValue(res) + res.status = jest.fn().mockReturnValue(res) + return res } describe("initUserController", () => { - it("should call createNewUserController if no user exists", async function () { - const querySpy = jest.spyOn(require("../users/create-new-user-controller"), "createNewUserController"); - (db.manyOrNone as any).mockResolvedValue([]) - const nextFunction: NextFunction = jest.fn() - const response = mockResponse() - const request = { body: { username: "test", password: "123" } } - await initUserController( - request as unknown as IGetUserAuthInfoRequest, - response as unknown as Response, nextFunction) - expect(querySpy).toHaveBeenCalledTimes(1) - expect(request.body).toEqual({ username: "test", password: "123", role: AllowedRoles.Admin }) - }) - it("should return an error if user already exists", async function () { - const querySpy = jest.spyOn(require("boom"), "forbidden"); - - (db.manyOrNone as any).mockResolvedValue(["test"]) - const nextFunction: NextFunction = jest.fn() - const response = mockResponse() - const request = {} - await initUserController( - request as unknown as IGetUserAuthInfoRequest, - response as unknown as Response, nextFunction) - expect(nextFunction).toHaveBeenCalledTimes(1) - expect(querySpy).toHaveBeenCalledTimes(1) - expect(querySpy).toHaveBeenCalledWith("User was already initialized") - }) + it("should call createNewUserController if no user exists", async function () { + const querySpy = jest.spyOn(require("../users/create-new-user-controller"), "createNewUserController"); + (db.oneOrNone as any).mockResolvedValueOnce({}); // migration has finished + (db.manyOrNone as any).mockResolvedValue([]) + const nextFunction: NextFunction = jest.fn() + const response = mockResponse() + const request = { body: { username: "test", password: "123" } } + await initUserController( + request as unknown as IGetUserAuthInfoRequest, + response as unknown as Response, nextFunction) + expect(querySpy).toHaveBeenCalledTimes(1) + expect(request.body).toEqual({ username: "test", password: "123", role: AllowedRoles.Admin }) + }) + it("should return an error if user already exists", async function () { + const querySpy = jest.spyOn(require("boom"), "forbidden"); + (db.oneOrNone as any).mockResolvedValueOnce({}); // migration has finished + (db.manyOrNone as any).mockResolvedValue(["test"]) + const nextFunction: NextFunction = jest.fn() + const response = mockResponse() + const request = {} + await initUserController( + request as unknown as IGetUserAuthInfoRequest, + response as unknown as Response, nextFunction) + expect(nextFunction).toHaveBeenCalledTimes(1) + expect(querySpy).toHaveBeenCalledTimes(1) + expect(querySpy).toHaveBeenCalledWith("User was already initialized") + }) + it("should throw exception when migrations have not finished", async () => { + const spy = jest.spyOn(require("boom"), "preconditionRequired") + const nextFunction: NextFunction = jest.fn() + const response = mockResponse() + const request = {} + await initUserController( + request as unknown as IGetUserAuthInfoRequest, + response as unknown as Response, nextFunction) + expect(nextFunction).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith("Migrations were not finished, please try it again later.") + }) }) diff --git a/src/server/controllers/auth/init-user-controller.ts b/src/server/controllers/auth/init-user-controller.ts index 67bd30ee..7c44ceb1 100644 --- a/src/server/controllers/auth/init-user-controller.ts +++ b/src/server/controllers/auth/init-user-controller.ts @@ -2,21 +2,28 @@ import boom = require("boom") import { Request, Response, NextFunction } from "express" import { db } from "../../../db/db" import { AllowedRoles } from "../../middleware/authorization-middleware" -import { getUsers } from "../../queries/auth" +import { getUsers, getRoleMigration } from "../../queries/auth" import { createNewUserController } from "../users/create-new-user-controller" +import { MigrationNotFinishedException } from "../../errors/migration-not-finished-exception" export const initUserController = async (req: Request, res: Response, next: NextFunction) => { - try { - const users = await db.manyOrNone(getUsers()) - if (users && users.length > 0) { - return next(boom.forbidden("User was already initialized")) - } - req.body.role = AllowedRoles.Admin - await createNewUserController(req, res, next) - + try { + const roleMigration = await db.oneOrNone(getRoleMigration()) + if (!roleMigration) { + throw new MigrationNotFinishedException("role migration has not finished") + } + const users = await db.manyOrNone(getUsers()) + if (users && users.length > 0) { + return next(boom.forbidden("User was already initialized")) + } + req.body.role = AllowedRoles.Admin + await createNewUserController(req, res, next) - } catch(error) { - return next(error) - } + } catch(error) { + if (error.code === "42P01" || error instanceof MigrationNotFinishedException) { + return next(boom.preconditionRequired("Migrations were not finished, please try it again later.")) + } + return next(error) + } } diff --git a/src/server/controllers/item/create-item-controller.ts b/src/server/controllers/item/create-item-controller.ts index 1aea7512..37f668ba 100644 --- a/src/server/controllers/item/create-item-controller.ts +++ b/src/server/controllers/item/create-item-controller.ts @@ -31,12 +31,13 @@ import { DataParsingException } from "../../errors/data-parsing-exception" import { itemErrorHandler } from "./shared/item-error-handler" const pg = pgp() +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +const FILE_LIMIT = 5120 * 1024 * 1024 const upload = multer( { dest: "./uploads", - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - limits: { fieldSize: 2048 * 1024 * 1024 }, + limits: { fieldSize: FILE_LIMIT }, }).fields([ { name: "kpi", maxCount: 1 }, { name: "monitoring", maxCount: 1 }, diff --git a/src/server/controllers/item/shared/item-data-processing.ts b/src/server/controllers/item/shared/item-data-processing.ts index 02433370..ccecccd6 100644 --- a/src/server/controllers/item/shared/item-data-processing.ts +++ b/src/server/controllers/item/shared/item-data-processing.ts @@ -32,7 +32,7 @@ import { } from "../../../queries/items" import { ReportStatus } from "../../../queries/items.model" import { getScenarioSettings } from "../../../queries/scenario" -import { sendNotifications } from "../../../utils/notifications/send-notification" +import { sendDegradationNotifications, sendReportNotifications } from "../../../utils/notifications/send-notification" import { scenarioThresholdsCalc } from "../utils/scenario-thresholds-calc" import { extraIntervalMilliseconds } from "./extra-intervals-mapping" import { AnalyticsEvent } from "../../../utils/analytics/anyltics-event" @@ -55,11 +55,13 @@ export const itemDataProcessing = async ({ projectName, scenarioName, itemId }) const responseFailures = await db.manyOrNone(responseMessageFailures(itemId)) const scenarioSettings = await db.one(getScenarioSettings(projectName, scenarioName)) - const rawData = await db.manyOrNone(findRawData(itemId)) - const rawDataArray = rawData?.map(row => [moment(row.timestamp).valueOf(), row.elapsed]) + let rawData = await db.manyOrNone(findRawData(itemId)) + let rawDataArray = rawData?.map(row => [moment(row.timestamp).valueOf(), row.elapsed]) const rawDataDownSampled = downsampleData(rawDataArray, MAX_SCATTER_CHART_POINTS) const groupedErrors = await db.manyOrNone(findGroupedErrors(itemId)) const top5ErrorsByLabel = await db.manyOrNone(findTop5ErrorsByLabel(itemId)) + rawData = null + rawDataArray = null if (aggOverview.number_of_sut_hostnames > 1) { sutMetrics = await db.many(sutOverviewQuery(itemId)) @@ -76,7 +78,6 @@ export const itemDataProcessing = async ({ projectName, scenarioName, itemId }) })) } - const { overview, overview: { duration }, @@ -139,11 +140,14 @@ export const itemDataProcessing = async ({ projectName, scenarioName, itemId }) const thresholdResult = scenarioThresholdsCalc(labelStats, baselineReport.stats, scenarioSettings) if (thresholdResult) { await db.none(saveThresholdsResult(projectName, scenarioName, itemId, thresholdResult)) + if (!thresholdResult.passed) { + await sendDegradationNotifications(projectName, scenarioName, itemId) + } } } } - await sendNotifications(projectName, scenarioName, itemId, overview) + await sendReportNotifications(projectName, scenarioName, itemId, overview) await db.tx(async t => { await t.none(saveItemStats(itemId, JSON.stringify(labelStats), diff --git a/src/server/controllers/item/utils/scenario-thresholds-calc.ts b/src/server/controllers/item/utils/scenario-thresholds-calc.ts index 573bf8d6..60b5a20c 100644 --- a/src/server/controllers/item/utils/scenario-thresholds-calc.ts +++ b/src/server/controllers/item/utils/scenario-thresholds-calc.ts @@ -4,7 +4,7 @@ import { divide } from "mathjs" const PERC = 100 // eslint-disable-next-line max-len -export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportStats: LabelStats[], scenarioSettings) => { +export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportStats: LabelStats[], scenarioSettings): ThresholdValidation => { const results = [] if (!scenarioSettings.errorRate || !scenarioSettings.percentile || !scenarioSettings.throughput) { return undefined @@ -71,3 +71,27 @@ export const scenarioThresholdsCalc = (labelStats: LabelStats[], baselineReportS } } +export interface ThresholdValidation { + passed: boolean + results: ThresholdResultExtended[] + thresholds: { + errorRate: number + throughput: number + percentile: number + } +} + +interface ThresholdResultExtended { + passed: boolean + label: string + result: { + percentile: ThresholdResult + throughput: ThresholdValidation + errorRate: ThresholdResult + } +} + +interface ThresholdResult { + passed: boolean + diffValue: boolean +} diff --git a/src/server/controllers/project/create-project-controller.spec.ts b/src/server/controllers/project/create-project-controller.spec.ts index 0f0ac31e..b2994b31 100644 --- a/src/server/controllers/project/create-project-controller.spec.ts +++ b/src/server/controllers/project/create-project-controller.spec.ts @@ -70,7 +70,8 @@ describe("createProjectController", function () { (db.one as any).mockResolvedValueOnce({ exists: false }); (db.one as any).mockResolvedValueOnce({ id: "123" }) - const dbNoneMock = (db.none as any).mockImplementationOnce(() => jest.fn()) + const dbNoneMock = (db.none as any).mockImplementationOnce(() => jest.fn()); + (db.manyOrNone as any).mockResolvedValueOnce([{ id: 831, role: "operator" }]) await createProjectController(request as unknown as IGetUserAuthInfoRequest, response as unknown as Response, next) diff --git a/src/server/controllers/project/create-project-controller.ts b/src/server/controllers/project/create-project-controller.ts index 3bec4b1b..f9fbb9b7 100644 --- a/src/server/controllers/project/create-project-controller.ts +++ b/src/server/controllers/project/create-project-controller.ts @@ -26,18 +26,24 @@ export const createProjectController = async (req: IGetUserAuthInfoRequest, res: await db.query(addProjectMember(project.id, req.user.userId)) } if (req.user.role === AllowedRoles.Admin && projectMembers?.length > 0) { - const columnSet = new pg.helpers.ColumnSet([ - { name: "project_id", prop: "projectId" }, - { name: "user_id", prop: "userId" }], - { table: new pg.helpers.TableName({ table: "user_project_access", schema: "jtl" }) }) - const dataToBeInserted = projectMembers.map(user => ({ - userId: user, - projectId: project.id, - })) - logger.info(`Granting access to following users ${projectMembers}`) - const query = pg.helpers.insert(dataToBeInserted, columnSet) - await db.none(query) - + logger.info(`Checking users roles, ${projectMembers}`) + const usersWithRoles = await db.manyOrNone( + "SELECT users.role, users.id FROM jtl.users users WHERE users.id IN ($1:list)", + [projectMembers]) + const nonAdminUsers = usersWithRoles.filter(user => user.role !== AllowedRoles.Admin) + if (nonAdminUsers && nonAdminUsers.length > 0) { + const columnSet = new pg.helpers.ColumnSet([ + { name: "project_id", prop: "projectId" }, + { name: "user_id", prop: "userId" }], + { table: new pg.helpers.TableName({ table: "user_project_access", schema: "jtl" }) }) + const dataToBeInserted = nonAdminUsers.map(user => ({ + userId: user.id, + projectId: project.id, + })) + logger.info(`Granting access to following users ${nonAdminUsers.map(user => user.id)}`) + const query = pg.helpers.insert(dataToBeInserted, columnSet) + await db.none(query) + } } } else { return next(boom.conflict("Project already exists")) diff --git a/src/server/controllers/scenario/notifications/create-notification-controller.ts b/src/server/controllers/scenario/notifications/create-notification-controller.ts index 33fe8713..3d86aa2f 100644 --- a/src/server/controllers/scenario/notifications/create-notification-controller.ts +++ b/src/server/controllers/scenario/notifications/create-notification-controller.ts @@ -5,7 +5,7 @@ import { StatusCode } from "../../../utils/status-code" export const createScenarioNotificationController = async (req: Request, res: Response) => { const { projectName, scenarioName } = req.params - const { type, url, name } = req.body - await db.none(createScenarioNotification(projectName, scenarioName, type, url, name)) + const { type, url, name, channel } = req.body + await db.none(createScenarioNotification(projectName, scenarioName, channel, url, name, type)) res.status(StatusCode.Created).send() } diff --git a/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.spec.ts b/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.spec.ts index bb8ced92..03f878ac 100644 --- a/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.spec.ts +++ b/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.spec.ts @@ -4,6 +4,7 @@ import { AllowedRoles } from "../../../middleware/authorization-middleware" import { v4 as uuidv4 } from "uuid" import { deleteScenarioShareTokenController } from "./delete-scenario-share-token-controller" import { db } from "../../../../db/db" +import { StatusCode } from "../../../utils/status-code" jest.mock("../../../../db/db") @@ -16,8 +17,12 @@ const mockResponse = () => { return res } +beforeEach(() => { + jest.clearAllMocks() +}) + describe("deleteScenarioShareTokenController", () => { - it("should be able to delete only my token whe user role is operator", async () => { + it("should be able to delete only my token when user role is operator", async () => { const response = mockResponse() const querySpy = jest.spyOn(require("../../../queries/scenario"), @@ -26,7 +31,7 @@ describe("deleteScenarioShareTokenController", () => { params: { projectName: "project", scenarioName: "scenario", shareTokenId: "my-share-token" }, user: { role: AllowedRoles.Operator, userId: uuidv4() }, }; - (db.oneOrNone as any).mockReturnValueOnce({} ); // dummy token search response + (db.oneOrNone as any).mockReturnValueOnce({}); // dummy token search response (db.none as any).mockReturnValueOnce() await deleteScenarioShareTokenController( @@ -42,6 +47,27 @@ describe("deleteScenarioShareTokenController", () => { request.user.userId) expect(response.send).toHaveBeenCalledTimes(1) }) + it("should return 404 when my token does not exist and user role is operator", async () => { + + const response = mockResponse() + const querySpy = jest.spyOn(require("../../../queries/scenario"), + "deleteMyScenarioShareToken") + const request = { + params: { projectName: "project", scenarioName: "scenario", shareTokenId: "my-share-token" }, + user: { role: AllowedRoles.Operator, userId: uuidv4() }, + }; + (db.oneOrNone as any).mockReturnValueOnce(null); // dummy token search response + (db.none as any).mockReturnValueOnce() + + await deleteScenarioShareTokenController( + request as unknown as IGetUserAuthInfoRequest, + response as unknown as Response) + + expect(querySpy).not.toHaveBeenCalled() + expect(response.send).toHaveBeenCalledTimes(1) + expect(response.status).toHaveBeenCalledWith(StatusCode.NotFound) + }) + it("should be able to delete any token whe user role is admin", async () => { const response = mockResponse() @@ -51,7 +77,7 @@ describe("deleteScenarioShareTokenController", () => { params: { projectName: "project", scenarioName: "scenario", shareTokenId: "my-share-token" }, user: { role: AllowedRoles.Admin, userId: uuidv4() }, }; - (db.oneOrNone as any).mockReturnValueOnce({} ); // dummy token search response + (db.oneOrNone as any).mockReturnValueOnce({}); // dummy token search response (db.none as any).mockReturnValueOnce() await deleteScenarioShareTokenController( @@ -67,5 +93,25 @@ describe("deleteScenarioShareTokenController", () => { ) expect(response.send).toHaveBeenCalledTimes(1) }) + it("should return 404 when share token id does not exist", async () => { + + const response = mockResponse() + const querySpy = jest.spyOn(require("../../../queries/scenario"), + "deleteScenarioShareToken") + const request = { + params: { projectName: "project", scenarioName: "scenario", shareTokenId: "my-share-token" }, + user: { role: AllowedRoles.Admin, userId: uuidv4() }, + }; + (db.oneOrNone as any).mockReturnValueOnce(null); // dummy token search response + (db.none as any).mockReturnValueOnce() + + await deleteScenarioShareTokenController( + request as unknown as IGetUserAuthInfoRequest, + response as unknown as Response) + + expect(querySpy).not.toHaveBeenCalled() + expect(response.send).toHaveBeenCalledTimes(1) + expect(response.status).toHaveBeenCalledWith(StatusCode.NotFound) + }) }) diff --git a/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.ts b/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.ts index 7edfea65..c0639eea 100644 --- a/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.ts +++ b/src/server/controllers/scenario/share-token/delete-scenario-share-token-controller.ts @@ -7,8 +7,9 @@ import { deleteMyScenarioShareToken, deleteScenarioShareToken, findMyScenarioShareToken, - findScenarioShareToken, + findScenarioShareTokenById, } from "../../../queries/scenario" +import { logger } from "../../../../logger" export const deleteScenarioShareTokenController = async (req: IGetUserAuthInfoRequest, res: Response) => { const { user } = req @@ -23,11 +24,12 @@ export const deleteScenarioShareTokenController = async (req: IGetUserAuthInfoRe res.status(StatusCode.NotFound).send() } else { const shareToken = await db.oneOrNone( - findScenarioShareToken(projectName, scenarioName, shareTokenId)) + findScenarioShareTokenById(projectName, scenarioName, shareTokenId)) if (shareToken) { await db.none(deleteScenarioShareToken(projectName, scenarioName, shareTokenId)) return res.status(StatusCode.Ok).send() } + logger.info(`Scenario token ${shareTokenId} not found. Cannot delete it.`) res.status(StatusCode.NotFound).send() } diff --git a/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.spec.ts b/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.spec.ts index 80251053..30d27e0c 100644 --- a/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.spec.ts +++ b/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.spec.ts @@ -30,7 +30,7 @@ describe("getScenarioShareTokenController", () => { expect(querySpy).toHaveBeenCalledTimes(1) expect(querySpy) .toHaveBeenLastCalledWith(request.params.projectName, request.params.scenarioName, request.user.userId) - expect(response.send).toHaveBeenCalledTimes(1) + expect(response.status).toHaveBeenCalledTimes(1) expect(response.json).toHaveBeenNthCalledWith(1, mockData) }) it("should return all tokens when user role is admin", async () => { diff --git a/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.ts b/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.ts index 2dd896cd..50865490 100644 --- a/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.ts +++ b/src/server/controllers/scenario/share-token/get-scenario-share-token-controller.ts @@ -10,8 +10,10 @@ export const getScenarioShareTokenController = async (req: IGetUserAuthInfoReque const { projectName, scenarioName } = req.params if ([AllowedRoles.Operator].includes(role)) { const myApiKeys = await db.manyOrNone(selectOnlyMyScenarioShareTokens(projectName, scenarioName, userId)) - return res.send(StatusCode.Ok).json(myApiKeys) + res.status(StatusCode.Ok).json(myApiKeys) + } else { + const shareTokens = await db.manyOrNone(searchScenarioShareTokens(projectName, scenarioName)) + res.status(StatusCode.Ok).json(shareTokens) } - const shareTokens = await db.manyOrNone(searchScenarioShareTokens(projectName, scenarioName)) - res.status(StatusCode.Ok).json(shareTokens) + } diff --git a/src/server/data-stats/prepare-data.spec.ts b/src/server/data-stats/prepare-data.spec.ts index 658cf9e8..908ebbc1 100644 --- a/src/server/data-stats/prepare-data.spec.ts +++ b/src/server/data-stats/prepare-data.spec.ts @@ -224,6 +224,8 @@ describe("prepare data", () => { bytes_sent_total: 69848465, total: 46596, n90: 271, + n95: 272, + n99: 273, number_of_failed: 3, } const labelsData = [ @@ -303,7 +305,9 @@ describe("prepare data", () => { const { overview, labelStats } = prepareDataForSavingToDb(overviewData, labelsData, [], statusCodes, responseFailures, apdex, groupedErrors, labelErrors) expect(overview).toEqual({ - percentil: 271, + percentile90: 271, + percentile95: 272, + percentile99: 273, maxVu: undefined, avgResponseTime: 106, errorRate: 0.01, diff --git a/src/server/data-stats/prepare-data.ts b/src/server/data-stats/prepare-data.ts index bf83d73a..d9cd7692 100644 --- a/src/server/data-stats/prepare-data.ts +++ b/src/server/data-stats/prepare-data.ts @@ -19,7 +19,9 @@ export const prepareDataForSavingToDb = (overviewData, labelData, sutStats, stat const endDate = new Date(overviewData.end) return { overview: { - percentil: roundNumberTwoDecimals(overviewData.n90), + percentile90: roundNumberTwoDecimals(overviewData.n90), + percentile95: roundNumberTwoDecimals(overviewData.n95), + percentile99: roundNumberTwoDecimals(overviewData.n99), maxVu: undefined, avgResponseTime: Math.round(overviewData.avg_response), errorRate: roundNumberTwoDecimals((overviewData.number_of_failed / overviewData.total) * 100), @@ -441,7 +443,9 @@ interface ChartLabelData { } export interface Overview { - percentil: number + percentile90: number + percentile95: number + percentile99: number errorRate: number errorCount: number throughput: number diff --git a/src/server/errors/migration-not-finished-exception.ts b/src/server/errors/migration-not-finished-exception.ts new file mode 100644 index 00000000..9f324ffe --- /dev/null +++ b/src/server/errors/migration-not-finished-exception.ts @@ -0,0 +1,9 @@ +export class MigrationNotFinishedException extends Error { + constructor (message) { + super(message) + + this.name = this.constructor.name + + Error.captureStackTrace(this, this.constructor) + } +} diff --git a/src/server/queries/auth.ts b/src/server/queries/auth.ts index 12dc45e7..45cab7a5 100644 --- a/src/server/queries/auth.ts +++ b/src/server/queries/auth.ts @@ -32,3 +32,10 @@ export const updatePassword = (id, password) => { values: [id, password], } } + +export const getRoleMigration = () => { + return { + text: "SELECT * FROM pgmigrations WHERE name = $1", + values: ["1643273224321_role"], + } +} diff --git a/src/server/queries/items.ts b/src/server/queries/items.ts index b28cf983..b0e1d014 100644 --- a/src/server/queries/items.ts +++ b/src/server/queries/items.ts @@ -241,6 +241,8 @@ export const aggOverviewQuery = (itemId) => { text: ` SELECT percentile_cont(0.90) within group (order by (samples.elapsed))::real as n90, + percentile_cont(0.95) within group (order by (samples.elapsed))::real as n95, + percentile_cont(0.99) within group (order by (samples.elapsed))::real as n99, COUNT(DISTINCT samples.hostname)::int number_of_hostnames, COUNT(DISTINCT samples.sut_hostname)::int number_of_sut_hostnames, MAX(samples.timestamp) as end, diff --git a/src/server/queries/scenario.ts b/src/server/queries/scenario.ts index 40f4d34c..22f59681 100644 --- a/src/server/queries/scenario.ts +++ b/src/server/queries/scenario.ts @@ -151,7 +151,7 @@ export const getProcessingItems = (projectName, scenarioName, environment) => { export const scenarioNotifications = (projectName, scenarioName) => { return { - text: `SELECT notif.id, url, type, notif.name FROM jtl.notifications as notif + text: `SELECT notif.id, url, channel, notification_type as "type", notif.name FROM jtl.notifications as notif LEFT JOIN jtl.scenario as s ON s.id = notif.scenario_id LEFT JOIN jtl.projects as p ON p.id = s.project_id WHERE s.name = $2 AND p.project_name = $1`, @@ -159,14 +159,24 @@ export const scenarioNotifications = (projectName, scenarioName) => { } } -export const createScenarioNotification = (projectName, scenarioName, type, url, name) => { +export const scenarioNotificationsByType = (projectName, scenarioName, type) => { return { - text: `INSERT INTO jtl.notifications(scenario_id, type, url, name) VALUES(( + text: `SELECT notif.id, url, channel, notif.name FROM jtl.notifications as notif + LEFT JOIN jtl.scenario as s ON s.id = notif.scenario_id + LEFT JOIN jtl.projects as p ON p.id = s.project_id + WHERE s.name = $2 AND p.project_name = $1 AND notification_type = $3`, + values: [projectName, scenarioName, type], + } +} + +export const createScenarioNotification = (projectName, scenarioName, channel, url, name, type) => { + return { + text: `INSERT INTO jtl.notifications(scenario_id, channel, url, name, notification_type) VALUES(( SELECT s.id FROM jtl.scenario as s LEFT JOIN jtl.projects as p ON p.id = s.project_id WHERE s.name = $2 AND p.project_name = $1 - ), $3, $4, $5)`, - values: [projectName, scenarioName, type, url, name], + ), $3, $4, $5, $6)`, + values: [projectName, scenarioName, channel, url, name, type], } } @@ -283,16 +293,28 @@ export const findScenarioShareToken = (projectName: string, scenarioName: string } } -export const findMyScenarioShareToken = (projectName: string, scenarioName: string, token: string, userId: string) => { +export const findScenarioShareTokenById = (projectName: string, scenarioName: string, id: string) => { + return { + text: `SELECT t.token FROM jtl.scenario_share_tokens as t + LEFT JOIN jtl.scenario as s ON s.id = t.scenario_id + LEFT JOIN jtl.projects as p ON p.id = s.project_id + WHERE p.project_name = $1 + AND s.name = $2 + AND t.id = $3;`, + values: [projectName, scenarioName, id], + } +} + +export const findMyScenarioShareToken = (projectName: string, scenarioName: string, tokenId: string, userId: string) => { return { text: `SELECT t.token FROM jtl.scenario_share_tokens as t LEFT JOIN jtl.scenario as s ON s.id = t.scenario_id LEFT JOIN jtl.projects as p ON p.id = s.project_id WHERE p.project_name = $1 AND s.name = $2 - AND t.token = $3 + AND t.id = $3 AND t.created_by = $4;`, - values: [projectName, scenarioName, token, userId], + values: [projectName, scenarioName, tokenId, userId], } } @@ -349,7 +371,7 @@ export const deleteMyScenarioShareToken = (projectName, scenarioName, id, userId text: `DELETE FROM jtl.scenario_share_tokens as sst USING jtl.scenario as sc WHERE sst.id = $3 - AND sst.created_by = $3 + AND sst.created_by = $4 AND sst.scenario_id = sc.id AND sc.name = $2 AND sc.project_id = (SELECT id FROM jtl.projects WHERE project_name = $1)`, diff --git a/src/server/schema-validator/project-schema.ts b/src/server/schema-validator/project-schema.ts index 3efc0f21..27be71e3 100644 --- a/src/server/schema-validator/project-schema.ts +++ b/src/server/schema-validator/project-schema.ts @@ -14,7 +14,9 @@ export const updateProjectSchema = { topMetricsSettings: Joi.object({ virtualUsers: Joi.boolean().required(), errorRate: Joi.boolean().required(), - percentile: Joi.boolean().required(), + percentile90: Joi.boolean().required(), + percentile95: Joi.boolean().required(), + percentile99: Joi.boolean().required(), throughput: Joi.boolean().required(), network: Joi.boolean().required(), avgLatency: Joi.boolean().required(), diff --git a/src/server/schema-validator/scenario-schema.ts b/src/server/schema-validator/scenario-schema.ts index d41afa10..d84f9f7c 100644 --- a/src/server/schema-validator/scenario-schema.ts +++ b/src/server/schema-validator/scenario-schema.ts @@ -35,8 +35,9 @@ export const querySchema = { export const scenarioNotificationBodySchema = { url: Joi.string().max(URL_MAX_LENGTH).required(), - type: Joi.string().valid(["ms-teams", "gchat", "slack"]).required(), + channel: Joi.string().valid(["ms-teams", "gchat", "slack"]).required(), name: Joi.string().min(1).max(MAX_NUMBER).required(), + type: Joi.string().valid("report_detail", "degradation").required(), } diff --git a/src/server/utils/analytics/analytics-event.spec.ts b/src/server/utils/analytics/analytics-event.spec.ts index 24d2c9c7..35258679 100644 --- a/src/server/utils/analytics/analytics-event.spec.ts +++ b/src/server/utils/analytics/analytics-event.spec.ts @@ -36,7 +36,7 @@ describe("AnalyticEvents", () => { expect(trackMock).not.toHaveBeenCalled() }) - it("should track the even only when analytics enabled", function () { + it("should track the event only when analytics enabled", function () { process.env.OPT_OUT_ANALYTICS = "false" const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) AnalyticsEvent.reportProcessingFinished() @@ -52,7 +52,7 @@ describe("AnalyticEvents", () => { expect(trackMock).not.toHaveBeenCalled() }) - it("should track the even only when analytics enabled", function () { + it("should track the event only when analytics enabled", function () { process.env.OPT_OUT_ANALYTICS = "false" const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) AnalyticsEvent.reportDetails(1, 1) @@ -68,12 +68,27 @@ describe("AnalyticEvents", () => { expect(trackMock).not.toHaveBeenCalled() }) - it("should track the even only when analytics enabled", function () { + it("should track the event only when analytics enabled", function () { process.env.OPT_OUT_ANALYTICS = "false" const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) AnalyticsEvent.reportProcessingStarted() expect(trackMock).toHaveBeenCalled() }) }) + describe("unexpectedError", () => { + it("should not track the event when analytics disabled", function () { + process.env.OPT_OUT_ANALYTICS = "true" + const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) + AnalyticsEvent.reportUnexpectedError(Error("test")) + expect(trackMock).not.toHaveBeenCalled() + + }) + it("should track the event only when analytics enabled", function () { + process.env.OPT_OUT_ANALYTICS = "false" + const trackMock = (analytics.track as any).mockResolvedValueOnce(undefined) + AnalyticsEvent.reportUnexpectedError(Error("test")) + expect(trackMock).toHaveBeenCalled() + }) + }) }) diff --git a/src/server/utils/analytics/anyltics-event.ts b/src/server/utils/analytics/anyltics-event.ts index d1580f7e..91dbf666 100644 --- a/src/server/utils/analytics/anyltics-event.ts +++ b/src/server/utils/analytics/anyltics-event.ts @@ -34,4 +34,13 @@ export class AnalyticsEvent { }) } } + + static reportUnexpectedError(error) { + if (this.isAnalyticEnabled()) { + analytics.track("unexpectedError", { + distinct_id: process.env.ANALYTICS_IDENTIFIER, + error, + }) + } + } } diff --git a/src/server/utils/notifications/send-notification.spec.ts b/src/server/utils/notifications/send-notification.spec.ts index 79e6da94..396ed1f3 100644 --- a/src/server/utils/notifications/send-notification.spec.ts +++ b/src/server/utils/notifications/send-notification.spec.ts @@ -1,17 +1,19 @@ import { db } from "../../../db/db" -import { sendNotifications } from "./send-notification" +import { sendReportNotifications } from "./send-notification" const linkUrl = require("./link-url") import axios from "axios" const scenarioNotifications = require("../../queries/scenario") -const msTeamsTemplate = require("./templates/ms-teams-template") +const msTeamsTemplate = require("./templates/report/ms-teams-template") jest.mock("axios") -jest.mock("./templates/ms-teams-template") +jest.mock("./templates/report/ms-teams-template") jest.mock("../../../db/db") const OVERVIEW = { - percentil: 10, + percentile90: 10, + percentile95: 14, + percentile99: 18, avgConnect: 1, avgLatency: 1, avgResponseTime: 1, @@ -29,34 +31,34 @@ const OVERVIEW = { describe("sendNotification", () => { it("should call linkUrl", async () => { const spy = jest.spyOn(linkUrl, "linkUrl") - await sendNotifications("test", "test", "id", OVERVIEW) + await sendReportNotifications("test", "test", "id", OVERVIEW) expect(spy).toHaveBeenCalledTimes(1) }) it("should trigger `scenarioNotifications` query", async () => { - const spy = jest.spyOn(scenarioNotifications, "scenarioNotifications") - await sendNotifications("test", "test", "id", OVERVIEW) + const spy = jest.spyOn(scenarioNotifications, "scenarioNotificationsByType") + await sendReportNotifications("test", "test", "id", OVERVIEW) expect(spy).toHaveBeenCalledTimes(1) }) it("should not send any request if no notifications found in db", async () => { db.manyOrNone = jest.fn().mockImplementation(() => Promise.resolve([])) - await sendNotifications("test", "test", "id", OVERVIEW) + await sendReportNotifications("test", "test", "id", OVERVIEW) expect(axios).not.toHaveBeenCalled() }) it("should try to send notification request when found in db", async () => { const spy = jest.spyOn(msTeamsTemplate, "msTeamsTemplate") db.manyOrNone = jest.fn().mockImplementation(() => - Promise.resolve([{ url: "test", name: "test-name", type: "ms-teams" }])) + Promise.resolve([{ url: "test", name: "test-name", channel: "ms-teams" }])) const post = axios.post = jest.fn().mockImplementation(() => Promise.resolve({})) - await sendNotifications("test", "test", "id", OVERVIEW) + await sendReportNotifications("test", "test", "id", OVERVIEW) expect(spy).toHaveBeenCalledTimes(1) expect(post).toHaveBeenCalledTimes(1) }) it("should not throw an error when request failed", () => { db.manyOrNone = jest.fn().mockImplementation(() => - Promise.resolve([{ url: "test", name: "test-name", type: "ms-teams" }])) + Promise.resolve([{ url: "test", name: "test-name", channel: "ms-teams" }])) axios.post = jest.fn().mockImplementation(() => Promise.reject(new Error("failed"))) expect(async () => { - await sendNotifications("test", "test", "id", OVERVIEW) + await sendReportNotifications("test", "test", "id", OVERVIEW) }).not.toThrow() }) }) diff --git a/src/server/utils/notifications/send-notification.ts b/src/server/utils/notifications/send-notification.ts index bdfb552b..0541fdc4 100644 --- a/src/server/utils/notifications/send-notification.ts +++ b/src/server/utils/notifications/send-notification.ts @@ -1,21 +1,25 @@ import { db } from "../../../db/db" -import { scenarioNotifications } from "../../queries/scenario" +import { scenarioNotificationsByType } from "../../queries/scenario" import axios from "axios" -import { msTeamsTemplate } from "./templates/ms-teams-template" +import { msTeamsTemplate } from "./templates/report/ms-teams-template" import { logger } from "../../../logger" import { linkUrl } from "./link-url" import { Overview } from "../../data-stats/prepare-data" -import { gchatTemplate } from "./templates/gchat-template" -import { slackTemplate } from "./templates/slack-template" +import { gchatTemplate } from "./templates/report/gchat-template" +import { slackTemplate } from "./templates/report/slack-template" +import { msTeamsDegradationTemplate } from "./templates/degradation/ms-teams-degradation-template" +import { gchatDegradationTemplate } from "./templates/degradation/gchat-degradation-template" +import { slackDegradationTemplate } from "./templates/degradation/slack-degradation-template" -export const sendNotifications = async (projectName, scenarioName, id, overview: Overview) => { +export const sendReportNotifications = async (projectName, scenarioName, id, overview: Overview) => { try { - const notifications: Notification[] = await db.manyOrNone(scenarioNotifications(projectName, scenarioName)) + const notifications: Notification[] = await db.manyOrNone( + scenarioNotificationsByType(projectName, scenarioName, NotificationType.ReportDetail)) const url = linkUrl(projectName, scenarioName, id) notifications.map(async (notif) => { - const messageTemplate = NotificationTemplate.get(notif.type) + const messageTemplate = NotificationReportTemplate.get(notif.channel) try { - logger.info(`sending notification to: ${notif.type}`) + logger.info(`sending notification to: ${notif.channel}`) const payload = messageTemplate(scenarioName, url, overview) await axios.post(notif.url, payload, { headers: { @@ -31,16 +35,53 @@ export const sendNotifications = async (projectName, scenarioName, id, overview: } } +export const sendDegradationNotifications = async (projectName, scenarioName, id) => { + try { + const notifications: Notification[] = await db.manyOrNone( + scenarioNotificationsByType(projectName, scenarioName, NotificationType.Degradation)) + const url = linkUrl(projectName, scenarioName, id) + notifications.map(async (notif) => { + const messageTemplate = NotificationDegradationTemplate.get(notif.channel) + try { + logger.info(`sending degradation notification to: ${notif.channel}`) + const payload = messageTemplate(scenarioName, url) + await axios.post(notif.url, payload, { + headers: { + "content-type": "application/json", + }, + }) + } catch(error) { + logger.error(`error while sending notification: ${error}`) + } + }) + } catch(error) { + logger.error(`Error notification processing: ${error}`) + } + +} + +enum NotificationType { + ReportDetail = "report_detail", + Degradation = "degradation", +} + interface Notification { id: string url: string - type: string + channel: string } -const NotificationTemplate = new Map([ +const NotificationReportTemplate = new Map([ ["ms-teams", msTeamsTemplate], ["gchat", gchatTemplate], ["slack", slackTemplate], ]) +const NotificationDegradationTemplate = + new Map([ + ["ms-teams", msTeamsDegradationTemplate], + ["gchat", gchatDegradationTemplate], + ["slack", slackDegradationTemplate], + ]) + diff --git a/src/server/utils/notifications/templates/degradation/gchat-degradation-template.spec.ts b/src/server/utils/notifications/templates/degradation/gchat-degradation-template.spec.ts new file mode 100644 index 00000000..d1b54446 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/gchat-degradation-template.spec.ts @@ -0,0 +1,55 @@ +import { gchatDegradationTemplate } from "./gchat-degradation-template" + +describe("GChat Degradation template", () => { + it("should return correct card payload when url provided", () => { + const template = gchatDegradationTemplate("scenarioName", "http://localhost") + expect(template).toEqual( { + cards: [ + { + header: { + imageUrl: "", + subtitle: "Performance Degradation Detected for scenario: scenarioName", + title: "JTL Reporter", + }, + sections: [ + { + widgets: [ + { + buttons: [ + { + textButton: { + onClick: { + openLink: { + url: "http://localhost", + }, + }, + text: "OPEN RESULTS", + }, + }, + ], + }, + ], + }, + ], + }, + ], + } + ) + }) + it("should return card payload when no url provided", () => { + const template = gchatDegradationTemplate("scenarioName", undefined) + expect(template).toEqual({ + cards: [ + { + header: { + imageUrl: "", + subtitle: "Performance Degradation Detected for scenario: scenarioName", + title: "JTL Reporter", + }, + sections: [], + }, + ], + } + ) + }) +}) diff --git a/src/server/utils/notifications/templates/degradation/gchat-degradation-template.ts b/src/server/utils/notifications/templates/degradation/gchat-degradation-template.ts new file mode 100644 index 00000000..a5feec16 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/gchat-degradation-template.ts @@ -0,0 +1,35 @@ + +export const gchatDegradationTemplate = (scenarioName: string, url) => { + const cardPayload = { + cards: [{ + header: { + title: "JTL Reporter", + subtitle: `Performance Degradation Detected for scenario: ${scenarioName}`, + imageUrl: "", + }, + sections: [], + }], + } + + if (url) { + (cardPayload.cards[0].sections as any).push({ + widgets: [ + { + buttons: [ + { + textButton: { + text: "OPEN RESULTS", + onClick: { + openLink: { + url, + }, + }, + }, + }, + ], + }, + ], + }) + } + return cardPayload +} diff --git a/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.spec.ts b/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.spec.ts new file mode 100644 index 00000000..8a48d405 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.spec.ts @@ -0,0 +1,55 @@ +import { msTeamsDegradationTemplate } from "./ms-teams-degradation-template" + +describe("MS Teams Degradation template", () => { + it("should return correct card payload when url provided", () => { + const template = msTeamsDegradationTemplate("scenarioName", "http://localhost") + expect(template).toEqual({ + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + contentUrl: null, + content: { + type: "AdaptiveCard", + body: [ + { + type: "TextBlock", + size: "Medium", + weight: "Bolder", + text: "Performance Degradation Detected for scenario: scenarioName", + }, + ], + actions: [ + { type: "Action.OpenUrl", title: "View", url: "http://localhost" }], + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.2", + }, + }], + }) + }) + it("should return card payload when no url provided", () => { + const template = msTeamsDegradationTemplate("scenarioName", undefined) + expect(template).toEqual({ + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + contentUrl: null, + content: { + type: "AdaptiveCard", + body: [ + { + type: "TextBlock", + size: "Medium", + weight: "Bolder", + text: "Performance Degradation Detected for scenario: scenarioName", + }, + ], + actions: [], + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.2", + }, + }], + }) + }) +}) diff --git a/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.ts b/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.ts new file mode 100644 index 00000000..7d3f69e9 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/ms-teams-degradation-template.ts @@ -0,0 +1,37 @@ +export const msTeamsDegradationTemplate = (scenarioName: string, url) => { + const cardPayload = { + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + contentUrl: null, + content: { + type: "AdaptiveCard", + body: [ + { + type: "TextBlock", + size: "Medium", + weight: "Bolder", + text: `Performance Degradation Detected for scenario: ${scenarioName}`, + }, + ], + actions: [], + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.2", + }, + + }, + ], + } + + if (url) { + cardPayload.attachments[0].content.actions.push({ + type: "Action.OpenUrl", + title: "View", + url, + }) + } + return cardPayload +} + + diff --git a/src/server/utils/notifications/templates/degradation/slack-degradation-template.spec.ts b/src/server/utils/notifications/templates/degradation/slack-degradation-template.spec.ts new file mode 100644 index 00000000..e087f1b5 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/slack-degradation-template.spec.ts @@ -0,0 +1,41 @@ +import { slackDegradationTemplate } from "./slack-degradation-template" + +describe("Slack Degradation template", () => { + it("should return correct card payload when url provided", () => { + const template = slackDegradationTemplate("scenarioName", "http://localhost") + console.log(JSON.stringify(template)) + expect(template).toEqual({ + blocks: [ + { type: "header", text: { type: "plain_text", text: "JTL Reporter" } }, + { + type: "header", text: { + type: "plain_text", + text: "Performance Degradation Detected for scenario: scenarioName", + }, + }, + { + type: "section", + fields: { + type: "button", + text: { type: "plain_text", text: "View", emoji: true }, + value: "click_me_123", + url: "http://localhost", + action_id: "button-action", + }, + }, + ], + } + ) + }) + it("should return card payload when no url provided", () => { + const template = slackDegradationTemplate("scenarioName", undefined) + expect(template).toEqual({ + blocks: [ + { type: "header", text: { type: "plain_text", text: "JTL Reporter" } }, + { + type: "header", + text: { type: "plain_text", text: "Performance Degradation Detected for scenario: scenarioName" }, + }], + }) + }) +}) diff --git a/src/server/utils/notifications/templates/degradation/slack-degradation-template.ts b/src/server/utils/notifications/templates/degradation/slack-degradation-template.ts new file mode 100644 index 00000000..f80a46d3 --- /dev/null +++ b/src/server/utils/notifications/templates/degradation/slack-degradation-template.ts @@ -0,0 +1,27 @@ +export const slackDegradationTemplate = (scenarioName: string, url) => { + const card = { + blocks: [ + { type: "header", text: { type: "plain_text", text: "JTL Reporter" } }, + { type: "header", text: { type: "plain_text", + text: `Performance Degradation Detected for scenario: ${scenarioName}` } }, + ], + } + + if (url) { + (card.blocks as any).push({ + type: "section", + fields: { + type: "button", + text: { + type: "plain_text", + text: "View", + emoji: true, + }, + value: "click_me_123", + url, + action_id: "button-action", + }, + }) + } + return card +} diff --git a/src/server/utils/notifications/templates/gchat-template.spec.ts b/src/server/utils/notifications/templates/report/gchat-template.spec.ts similarity index 98% rename from src/server/utils/notifications/templates/gchat-template.spec.ts rename to src/server/utils/notifications/templates/report/gchat-template.spec.ts index 8974db20..371d4613 100644 --- a/src/server/utils/notifications/templates/gchat-template.spec.ts +++ b/src/server/utils/notifications/templates/report/gchat-template.spec.ts @@ -2,7 +2,9 @@ import { gchatTemplate } from "./gchat-template" describe("GChat template", () => { const OVERVIEW = { - percentil: 10, + percentile90: 10, + percentile95: 14, + percentile99: 18, avgConnect: 1, avgLatency: 1, avgResponseTime: 1, diff --git a/src/server/utils/notifications/templates/gchat-template.ts b/src/server/utils/notifications/templates/report/gchat-template.ts similarity index 92% rename from src/server/utils/notifications/templates/gchat-template.ts rename to src/server/utils/notifications/templates/report/gchat-template.ts index 96122415..5b953a44 100644 --- a/src/server/utils/notifications/templates/gchat-template.ts +++ b/src/server/utils/notifications/templates/report/gchat-template.ts @@ -1,4 +1,4 @@ -import { Overview } from "../../../data-stats/prepare-data" +import { Overview } from "../../../../data-stats/prepare-data" export const gchatTemplate = (scenarioName: string, url, overview: Overview) => { const cardPayload = { @@ -14,7 +14,7 @@ export const gchatTemplate = (scenarioName: string, url, overview: Overview) => { keyValue: { topLabel: "90% percentile", - content: `${overview.percentil} ms`, + content: `${overview.percentile90} ms`, }, }, { keyValue: { topLabel: "Throughput", content: `${overview.throughput} reqs/s` } }, diff --git a/src/server/utils/notifications/templates/ms-teams-template.spec.ts b/src/server/utils/notifications/templates/report/ms-teams-template.spec.ts similarity index 97% rename from src/server/utils/notifications/templates/ms-teams-template.spec.ts rename to src/server/utils/notifications/templates/report/ms-teams-template.spec.ts index fb8b3f3f..3a9d4a82 100644 --- a/src/server/utils/notifications/templates/ms-teams-template.spec.ts +++ b/src/server/utils/notifications/templates/report/ms-teams-template.spec.ts @@ -2,7 +2,9 @@ import { msTeamsTemplate } from "./ms-teams-template" describe("MS Teams template", () => { const OVERVIEW = { - percentil: 10, + percentile90: 10, + percentile95: 14, + percentile99: 18, avgConnect: 1, avgLatency: 1, avgResponseTime: 1, diff --git a/src/server/utils/notifications/templates/ms-teams-template.ts b/src/server/utils/notifications/templates/report/ms-teams-template.ts similarity index 92% rename from src/server/utils/notifications/templates/ms-teams-template.ts rename to src/server/utils/notifications/templates/report/ms-teams-template.ts index 1a629847..289173c5 100644 --- a/src/server/utils/notifications/templates/ms-teams-template.ts +++ b/src/server/utils/notifications/templates/report/ms-teams-template.ts @@ -1,4 +1,4 @@ -import { Overview } from "../../../data-stats/prepare-data" +import { Overview } from "../../../../data-stats/prepare-data" export const msTeamsTemplate = (scenarioName: string, url, overview: Overview) => { const cardPayload = { @@ -25,7 +25,7 @@ export const msTeamsTemplate = (scenarioName: string, url, overview: Overview) = }, { title: "90% percentile", - value: overview.percentil + " ms", + value: overview.percentile90 + " ms", }, { title: "Throughput", diff --git a/src/server/utils/notifications/templates/slack-template.spec.ts b/src/server/utils/notifications/templates/report/slack-template.spec.ts similarity index 97% rename from src/server/utils/notifications/templates/slack-template.spec.ts rename to src/server/utils/notifications/templates/report/slack-template.spec.ts index 6658bc1a..82d2d66a 100644 --- a/src/server/utils/notifications/templates/slack-template.spec.ts +++ b/src/server/utils/notifications/templates/report/slack-template.spec.ts @@ -2,7 +2,9 @@ import { slackTemplate } from "./slack-template" describe("Slack template", () => { const OVERVIEW = { - percentil: 10, + percentile90: 10, + percentile95: 14, + percentile99: 18, avgConnect: 1, avgLatency: 1, avgResponseTime: 1, diff --git a/src/server/utils/notifications/templates/slack-template.ts b/src/server/utils/notifications/templates/report/slack-template.ts similarity index 91% rename from src/server/utils/notifications/templates/slack-template.ts rename to src/server/utils/notifications/templates/report/slack-template.ts index ceda48f2..52767b3b 100644 --- a/src/server/utils/notifications/templates/slack-template.ts +++ b/src/server/utils/notifications/templates/report/slack-template.ts @@ -1,4 +1,4 @@ -import { Overview } from "../../../data-stats/prepare-data" +import { Overview } from "../../../../data-stats/prepare-data" export const slackTemplate = (scenarioName: string, url, overview: Overview) => { const card = { @@ -8,7 +8,7 @@ export const slackTemplate = (scenarioName: string, url, overview: Overview) => }, { type: "divider" }, { type: "section", fields: [{ type: "mrkdwn", text: `*Error Rate*\n${overview.errorRate}%` }], - }, { type: "section", fields: [{ type: "mrkdwn", text: `*90% percentile*\n${overview.percentil} ms` }] }, { + }, { type: "section", fields: [{ type: "mrkdwn", text: `*90% percentile*\n${overview.percentile90} ms` }] }, { type: "section", fields: [{ type: "mrkdwn", text: `*Throughput*\n${overview.throughput} reqs/s` }], }, { type: "section", fields: [{ type: "mrkdwn", text: `*Duration*\n${overview.duration} min` }] }],