diff --git a/.circleci/config.yml b/.circleci/config.yml index 7401881..d2a7799 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,42 +1,24 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/2.0/configuration-reference version: 2.1 - -orbs: - # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize - # Orbs reduce the amount of configuration required for common tasks. - # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node - node: circleci/node@4.7 +executors: + runner: + machine: + image: "ubuntu-2004:current" + docker_layer_caching: true jobs: - # Below is the definition of your job to build and test your app, you can rename and customize it as you want. - test: - # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ - # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. - # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node - docker: - - image: cimg/node:16.10 - # Then run your tests! - # CircleCI will report the results back to your VCS provider. + build: + executor: runner steps: - # Checkout the code as the first step. - checkout - # Next, the node orb's install-packages step will install the dependencies from a package.json. - # The orb install-packages step will also automatically cache them for faster future runs. - - node/install-packages: - # If you are using yarn, change the line below from "npm" to "yarn" - pkg-manager: npm - run: - name: Run tests - command: npm test - -workflows: - # Below is the definition of your workflow. - # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. - # CircleCI will run this workflow on every commit. - # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows - test: - jobs: - - test - # For running simple node tests, you could optionally use the node/test job from the orb to replicate and replace the job above in fewer lines. - # - node/test + name: Test for ENV + command: | + a=123 + - run: + name: Check docker compose config + command: | + docker-compose config + - run: + name: Run tests using `docker-compose` + command: | + docker-compose up --build --exit-code-from test diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9bd63d4..ec463df 100644 --- a/.gitignore +++ b/.gitignore @@ -113,5 +113,4 @@ config.js env-config.ts # Astthors txt notes -www-astthor-notes.txt -authAstthorTesting.routes.ts \ No newline at end of file +Astthor-performance-test.txt diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e300c4b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3' + +services: + test: + build: + context: . + dockerfile: ./docker/app/Dockerfile + network_mode: "host" + environment: + - PORT=$PORT + - DATABASE=$DATABASE + - DB_USERNAME=$DB_USERNAME + - DB_PASSWORD=$DB_PASSWORD + - DB_HOST=$DB_HOST + - MICROSOFT_CLIENT_ID=$MICROSOFT_CLIENT_ID + - MICROSOFT_CLIENT_SECRET=$MICROSOFT_CLIENT_SECRET + - MICROSOFT_CALL_BACK_URL=$MICROSOFT_CALL_BACK_URL + - MICROSOFT_SCOPE=$MICROSOFT_SCOPE + - SESSION_SECRET=$SESSION_SECRET + - SESSION_RESAVE=$SESSION_RESAVE + - SESSION_SAVEUNINITIALIZED=$SESSION_SAVEUNINITIALIZED + - SESSION_COOKIE_SECURE=$SESSION_COOKIE_SECURE + - FRONTEND_APP=$FRONTEND_APP + # ports: + # - 4200:4200 + depends_on: + mysql: + condition: service_healthy + mysql: + image: mysql:latest + network_mode: "host" + environment: + - MYSQL_ROOT_PASSWORD=$DB_PASSWORD + - MYSQL_DATABASE=$DATABASE + - MYSQL_USER=$DB_USERNAME + - MYSQL_PASSWORD=$DB_PASSWORD + # ports: + # - 3306:3306 + security_opt: + - seccomp:unconfined + healthcheck: + test: ["CMD-SHELL", "echo 'HELLO WORLD' "] + start_period: 60s diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 0000000..e16f259 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,23 @@ +# Lightweight environment with node pre-installed +FROM node:alpine AS builder + +# Directory to work in the container +WORKDIR /app + +# Copy package.json to the root of the WORKDIR +COPY package.json ./ + +# Copy all files different to local machine to container +# The way caching works, any changes to any files will force everything under it to +# rebuild all subsequent steps +COPY . . + +# Run npm install inside container +# --legacy-peer-deps This line is currently required for the dependencies to install +# Should be tested once in a while and removed when possible, as package maintainers +# updated their dependencies to the new automated system. +RUN npm ci + +# Execute this command in the docker container + +CMD ["npm", "test"] diff --git a/docker/mysql/Dockerfile b/docker/mysql/Dockerfile new file mode 100644 index 0000000..0bec676 --- /dev/null +++ b/docker/mysql/Dockerfile @@ -0,0 +1 @@ +FROM mysql:latest diff --git a/docs/population-script.sql b/docs/population-script.sql index 308d701..4c9b1c8 100644 --- a/docs/population-script.sql +++ b/docs/population-script.sql @@ -8,66 +8,66 @@ INSERT INTO roles VALUES(2, "student"); -- Users --------------------------------------------------------------------- -- Teachers -INSERT INTO users VALUES(1, 'Teacher Bob', 'bob@kea.dk', 1); +INSERT INTO users VALUES(1, 'Teacher Bob', 'alex320i@stud.kea.dk', 1); INSERT INTO users VALUES(2, 'Teacher Ann', 'ann@kea.dk', 1); INSERT INTO users VALUES(3, 'Teacher Won', 'won@kea.dk', 1); INSERT INTO users VALUES(4, 'Teacher Tom', 'tom@kea.dk', 1); -- Students -INSERT INTO users VALUES(6, 'Student Ada', 'ada@stud.kea.dk', 2); -INSERT INTO users VALUES(7, 'Student Pam', 'pam@stud.kea.dk', 2); -INSERT INTO users VALUES(8, 'Student Kit', 'kit@stud.kea.dk', 2); -INSERT INTO users VALUES(9, 'Student Zoe', 'zoe@stud.kea.dk', 2); -INSERT INTO users VALUES(10, 'Student Ray', 'ray@stud.kea.dk', 2); -INSERT INTO users VALUES(11, 'Student Alf', 'alf@stud.kea.dk', 2); -INSERT INTO users VALUES(12, 'Student Coy', 'coy@stud.kea.dk', 2); -INSERT INTO users VALUES(13, 'Student Gil', 'gil@stud.kea.dk', 2); +INSERT INTO users VALUES(6, 'Student Ada', 'cris2041@stud.kea.dk', 2); +INSERT INTO users VALUES(7, 'Student Pam', 'pam@stud.kea.dk', 2); +INSERT INTO users VALUES(8, 'Student Kit', 'kit@stud.kea.dk', 2); +INSERT INTO users VALUES(9, 'Student Zoe', 'zoe@stud.kea.dk', 2); +INSERT INTO users VALUES(10, 'Student Ray', 'ray@stud.kea.dk', 2); +INSERT INTO users VALUES(11, 'Student Alf', 'alf@stud.kea.dk', 2); +INSERT INTO users VALUES(12, 'Student Coy', 'coy@stud.kea.dk', 2); +INSERT INTO users VALUES(13, 'Student Gil', 'gil@stud.kea.dk', 2); -- Classes --------------------------------------------------------------------- -INSERT INTO classes VALUES(1, "SW20"); -INSERT INTO classes VALUES(2, "WD20"); -INSERT INTO classes VALUES(3, "SW21"); -INSERT INTO classes VALUES(4, "WD21"); -INSERT INTO classes VALUES(5, "SW22"); -INSERT INTO classes VALUES(6, "WD22"); +INSERT INTO classes VALUES(1, "SW20"); +INSERT INTO classes VALUES(2, "WD20"); +INSERT INTO classes VALUES(3, "SW21"); +INSERT INTO classes VALUES(4, "WD21"); +INSERT INTO classes VALUES(5, "SW22"); +INSERT INTO classes VALUES(6, "WD22"); -- Subjects -------------------------------------------------------------------- -INSERT INTO subjects VALUES(1, "Testing SW20", 1, 1); -INSERT INTO subjects VALUES(2, "Testing SW21", 1, 3); -INSERT INTO subjects VALUES(3, "Testing SW22", 1, 5); -INSERT INTO subjects VALUES(4, "Web Development WD20", 2, 2); -INSERT INTO subjects VALUES(5, "Web Development WD21", 2, 4); -INSERT INTO subjects VALUES(6, "Web Development WD22", 2, 6); -INSERT INTO subjects VALUES(7, "Databases SW20", 3, 1); -INSERT INTO subjects VALUES(8, "Databases SW21", 3, 3); -INSERT INTO subjects VALUES(9, "Databases SW22", 3, 5); -INSERT INTO subjects VALUES(10, "Large Systems SW20", 4, 1); -INSERT INTO subjects VALUES(11, "Large Systems SW21", 4, 3); -INSERT INTO subjects VALUES(12, "Large Systems SW22", 4, 5); +INSERT INTO subjects VALUES(1, "Testing", 1, 1); +INSERT INTO subjects VALUES(2, "Testing", 1, 3); +INSERT INTO subjects VALUES(3, "Testing", 1, 5); +INSERT INTO subjects VALUES(4, "Web Development", 2, 2); +INSERT INTO subjects VALUES(5, "Web Development", 2, 4); +INSERT INTO subjects VALUES(6, "Web Development", 2, 6); +INSERT INTO subjects VALUES(7, "Databases", 3, 1); +INSERT INTO subjects VALUES(8, "Databases", 3, 3); +INSERT INTO subjects VALUES(9, "Databases", 3, 5); +INSERT INTO subjects VALUES(10, "Large Systems", 4, 1); +INSERT INTO subjects VALUES(11, "Large Systems", 4, 3); +INSERT INTO subjects VALUES(12, "Large Systems", 4, 5); -- Lectures -------------------------------------------------------------------- -INSERT INTO lectures VALUES(1, "Not Learning Microservices 1", NOW(), (NOW() + interval 90 minute), 12); -INSERT INTO lectures VALUES(2, "Not Learning Microservices 2", (NOW() + interval 90 minute), (NOW() + interval 180 minute), 12); -INSERT INTO lectures VALUES(3, "Not Learning Microservices 3", (NOW() + interval 180 minute), (NOW() + interval 270 minute), 12); -INSERT INTO lectures VALUES(4, "NoSQL 1", NOW(), (NOW() + interval 90 minute), 9); -INSERT INTO lectures VALUES(5, "NoSQL 2", (NOW() + interval 90 minute), (NOW() + interval 180 minute), 9); -INSERT INTO lectures VALUES(6, "NoSQL 3", (NOW() + interval 180 minute), (NOW() + interval 270 minute), 9); -INSERT INTO lectures VALUES(7, "Unit Testing", NOW(), (NOW() + interval 90 minute), 3); -INSERT INTO lectures VALUES(8, "Unit Testing", (NOW() + interval 90 minute), (NOW() + interval 180 minute), 3); -INSERT INTO lectures VALUES(9, "Unit Testing", (NOW() + interval 180 minute), (NOW() + interval 270 minute), 3); +INSERT INTO lectures VALUES(1, "Not Learning Microservices 1", '2022-06-06 13:14', ('2022-06-06 13:14' + interval 90 minute), 12); +INSERT INTO lectures VALUES(2, "Not Learning Microservices 2", '2022-06-06 13:18', ('2022-06-06 13:18' + interval 180 minute), 12); +INSERT INTO lectures VALUES(3, "Not Learning Microservices 3", '2022-06-06 13:22', ('2022-06-06 13:22' + interval 270 minute), 12); +INSERT INTO lectures VALUES(4, "NoSQL 1", '2022-06-06 13:26', ('2022-06-06 13:26' + interval 90 minute), 9); +INSERT INTO lectures VALUES(5, "NoSQL 2", '2022-06-06 13:30', ('2022-06-06 13:30' + interval 180 minute), 9); +INSERT INTO lectures VALUES(6, "NoSQL 3", '2022-06-06 13:34', ('2022-06-06 13:34' + interval 270 minute), 9); +INSERT INTO lectures VALUES(7, "Unit Testing", '2022-06-06 13:40', ('2022-06-06 13:40' + interval 90 minute), 3); +INSERT INTO lectures VALUES(8, "Unit Testing", '2022-06-06 13:44', ('2022-06-06 13:44' + interval 180 minute), 3); +INSERT INTO lectures VALUES(9, "Unit Testing", '2022-06-06 13:50', ('2022-06-06 13:50' + interval 270 minute), 3); -- Attendance ------------------------------------------------------------------- -INSERT INTO attendances VALUES(1, NOW(), 1, 5); -INSERT INTO attendances VALUES(2, NOW(), 1, 6); -INSERT INTO attendances VALUES(3, NOW(), 1, 7); -INSERT INTO attendances VALUES(4, NOW(), 1, 8); -INSERT INTO attendances VALUES(5, NOW(), 1, 9); -INSERT INTO attendances VALUES(6, (NOW() + interval 95 minute), 2, 5); -INSERT INTO attendances VALUES(7, (NOW() + interval 96 minute), 2, 6); -INSERT INTO attendances VALUES(8, (NOW() + interval 97 minute), 2, 7); -INSERT INTO attendances VALUES(9, (NOW() + interval 98 minute), 2, 8); -INSERT INTO attendances VALUES(10, (NOW() + interval 95 minute), 2, 5); -INSERT INTO attendances VALUES(11, (NOW() + interval 185 minute), 2, 5); -INSERT INTO attendances VALUES(12, (NOW() + interval 186 minute), 2, 6); -INSERT INTO attendances VALUES(13, (NOW() + interval 187 minute), 2, 7); +INSERT INTO attendances VALUES(1, '2022-06-06 13:14', 1, 5); +INSERT INTO attendances VALUES(2, '2022-06-06 13:14', 1, 6); +INSERT INTO attendances VALUES(3, '2022-06-06 13:14', 1, 7); +INSERT INTO attendances VALUES(4, '2022-06-06 13:14', 1, 8); +INSERT INTO attendances VALUES(5, '2022-06-06 13:14', 1, 9); +INSERT INTO attendances VALUES(6, '2022-06-06 13:14', 2, 5); +INSERT INTO attendances VALUES(7, '2022-06-06 13:14', 2, 6); +INSERT INTO attendances VALUES(8, '2022-06-06 13:14', 2, 7); +INSERT INTO attendances VALUES(9, '2022-06-06 13:14', 2, 8); +INSERT INTO attendances VALUES(10, '2022-06-06 13:14', 2, 5); +INSERT INTO attendances VALUES(11, '2022-06-06 13:14', 2, 5); +INSERT INTO attendances VALUES(12, '2022-06-06 13:14', 2, 6); +INSERT INTO attendances VALUES(13, '2022-06-06 13:14', 2, 7); diff --git a/jest.config.js b/jest.config.js index 05d68ec..0c36e02 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", modulePathIgnorePatterns: ["build"], + testTimeout: 15000 }; diff --git a/package-lock.json b/package-lock.json index a6114c8..16c2ecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "kea-large-systems-backend", "version": "0.0.1", + "hasInstallScript": true, "license": "MIT", "dependencies": { "@types/express": "^4.17.13", @@ -16,12 +17,11 @@ "express": "^4.17.3", "express-session": "^1.17.2", "helmet": "^5.0.2", + "moment": "^2.29.3", "mysql2": "^2.3.3", "passport": "^0.5.2", "passport-microsoft": "^1.0.0", - "sequelize": "^6.19.0", - "tsc": "^2.0.4", - "typescript": "^4.7.2" + "sequelize": "^6.19.0" }, "devDependencies": { "@types/cors": "^2.8.12", @@ -29,9 +29,13 @@ "@types/jest": "^27.4.1", "@types/passport": "^1.0.7", "@types/sequelize": "^4.28.11", + "@types/supertest": "^2.0.12", "nodemon": "^2.0.15", + "prettier": "^2.6.2", + "supertest": "^6.2.3", "ts-jest": "^27.1.4", - "ts-node": "^10.5.0" + "ts-node": "^10.5.0", + "typescript": "^4.7.2" } }, "node_modules/@ampproject/remapping": { @@ -1182,6 +1186,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "node_modules/@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -1261,9 +1271,9 @@ } }, "node_modules/@types/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==", + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dev": true, "dependencies": { "jest-matcher-utils": "^27.0.0", @@ -1287,14 +1297,14 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.39.tgz", + "integrity": "sha512-JDU3YLlnPK3WDao6/DlXLOgSNpG13ct+CwIO17V8q0/9fWJyeMJJ/VyZ1lv8kDprihvZMydzVwf0tQOqGiY2Nw==" }, "node_modules/@types/passport": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", - "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.8.tgz", + "integrity": "sha512-Gdcvis7+7G/Mobm+25AeFi+oe5teBhHzpbCOFWeN10Bj8tnoEE1L5lkraQjzmDEKkJQuM7xSJUGIFGl/giyRfQ==", "dev": true, "dependencies": { "@types/express": "*" @@ -1345,6 +1355,25 @@ "dev": true, "peer": true }, + "node_modules/@types/superagent": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.15.tgz", + "integrity": "sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "dependencies": { + "@types/superagent": "*" + } + }, "node_modules/@types/validator": { "version": "13.7.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.2.tgz", @@ -1557,12 +1586,17 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "peer": true + "dev": true }, "node_modules/babel-jest": { "version": "27.5.1", @@ -1924,9 +1958,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==", + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==", "dev": true, "funding": [ { @@ -2080,7 +2114,6 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2088,6 +2121,12 @@ "node": ">= 0.8" } }, + "node_modules/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==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2160,6 +2199,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "dev": true + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2315,7 +2360,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "peer": true, "engines": { "node": ">=0.4.0" } @@ -2355,6 +2399,16 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2433,9 +2487,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==", + "version": "1.4.146", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.146.tgz", + "integrity": "sha512-4eWebzDLd+hYLm4csbyMU2EbBnqhwl8Oe9eF/7CBDPWcRxFmqzx4izxvHH+lofQxzieg8UbB8ZuzNTxeukzfTg==", "dev": true, "peer": true }, @@ -2714,6 +2768,12 @@ "dev": true, "peer": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -2782,6 +2842,33 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", + "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "dev": true, + "dependencies": { + "dezalgo": "1.0.3", + "hexoid": "1.0.0", + "once": "1.4.0", + "qs": "6.9.3" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/formidable/node_modules/qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3034,6 +3121,15 @@ "node": ">=12.0.0" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -4452,7 +4548,7 @@ "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { "node": ">= 0.6" } @@ -4460,7 +4556,7 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -4472,7 +4568,7 @@ "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "engines": { "node": ">= 0.6" } @@ -4580,7 +4676,7 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/mysql2": { "version": "2.3.3", @@ -4639,7 +4735,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "peer": true }, @@ -4654,7 +4750,7 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "peer": true }, @@ -4742,7 +4838,7 @@ "node_modules/nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "dev": true, "dependencies": { "abbrev": "1" @@ -4795,12 +4891,12 @@ "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } @@ -4835,7 +4931,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" @@ -5022,7 +5118,7 @@ "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", "engines": { "node": ">= 0.4.0" } @@ -5040,7 +5136,7 @@ "node_modules/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=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "peer": true, "engines": { @@ -5067,12 +5163,12 @@ "node_modules/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=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/pg-connection-string": { "version": "2.5.0", @@ -5124,7 +5220,7 @@ "node_modules/pkginfo": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", - "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=", + "integrity": "sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==", "engines": { "node": ">= 0.4.0" } @@ -5132,7 +5228,7 @@ "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true, "peer": true, "engines": { @@ -5142,12 +5238,27 @@ "node_modules/prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true, "engines": { "node": ">=4" } }, + "node_modules/prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -5203,7 +5314,7 @@ "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" }, "node_modules/psl": { "version": "1.8.0", @@ -5267,7 +5378,7 @@ "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", "engines": { "node": ">= 0.8" } @@ -5330,6 +5441,20 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "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==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5784,6 +5909,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5857,6 +5991,105 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.6.tgz", + "integrity": "sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supertest": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.3.tgz", + "integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^7.1.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6092,9 +6325,9 @@ } }, "node_modules/ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -6143,14 +6376,6 @@ "node": ">=0.4.0" } }, - "node_modules/tsc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz", - "integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==", - "bin": { - "tsc": "bin/tsc" - } - }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -6209,9 +6434,10 @@ } }, "node_modules/typescript": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", - "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6327,6 +6553,12 @@ "node": ">=4" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -6365,9 +6597,9 @@ } }, "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "peer": true, "engines": { @@ -7572,6 +7804,12 @@ "@types/node": "*" } }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -7651,9 +7889,9 @@ } }, "@types/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==", + "version": "27.5.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", + "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dev": true, "requires": { "jest-matcher-utils": "^27.0.0", @@ -7677,14 +7915,14 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.39.tgz", + "integrity": "sha512-JDU3YLlnPK3WDao6/DlXLOgSNpG13ct+CwIO17V8q0/9fWJyeMJJ/VyZ1lv8kDprihvZMydzVwf0tQOqGiY2Nw==" }, "@types/passport": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.7.tgz", - "integrity": "sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.8.tgz", + "integrity": "sha512-Gdcvis7+7G/Mobm+25AeFi+oe5teBhHzpbCOFWeN10Bj8tnoEE1L5lkraQjzmDEKkJQuM7xSJUGIFGl/giyRfQ==", "dev": true, "requires": { "@types/express": "*" @@ -7735,6 +7973,25 @@ "dev": true, "peer": true }, + "@types/superagent": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.15.tgz", + "integrity": "sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.12.tgz", + "integrity": "sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/validator": { "version": "13.7.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.2.tgz", @@ -7904,12 +8161,17 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "peer": true + "dev": true }, "babel-jest": { "version": "27.5.1", @@ -8180,9 +8442,9 @@ "peer": true }, "caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==", + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==", "dev": true, "peer": true }, @@ -8293,11 +8555,16 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "peer": true, "requires": { "delayed-stream": "~1.0.0" } }, + "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==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8360,6 +8627,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "cookiejar": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", + "dev": true + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -8492,8 +8765,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "peer": true + "dev": true }, "denque": { "version": "2.0.1", @@ -8517,6 +8789,16 @@ "dev": true, "peer": true }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "dev": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -8579,9 +8861,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==", + "version": "1.4.146", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.146.tgz", + "integrity": "sha512-4eWebzDLd+hYLm4csbyMU2EbBnqhwl8Oe9eF/7CBDPWcRxFmqzx4izxvHH+lofQxzieg8UbB8ZuzNTxeukzfTg==", "dev": true, "peer": true }, @@ -8798,6 +9080,12 @@ "dev": true, "peer": true }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -8854,6 +9142,26 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz", + "integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==", + "dev": true, + "requires": { + "dezalgo": "1.0.3", + "hexoid": "1.0.0", + "once": "1.4.0", + "qs": "6.9.3" + }, + "dependencies": { + "qs": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz", + "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==", + "dev": true + } + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9035,6 +9343,12 @@ "resolved": "https://registry.npmjs.org/helmet/-/helmet-5.1.0.tgz", "integrity": "sha512-klsunXs8rgNSZoaUrNeuCiWUxyc+wzucnEnFejUg3/A+CaF589k9qepLZZ1Jehnzig7YbD4hEuscGXuBY3fq+g==" }, + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "dev": true + }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -10141,12 +10455,12 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -10158,7 +10472,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.5", @@ -10233,7 +10547,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "mysql2": { "version": "2.3.3", @@ -10287,7 +10601,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "peer": true }, @@ -10299,7 +10613,7 @@ "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "peer": true }, @@ -10369,7 +10683,7 @@ "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", "dev": true, "requires": { "abbrev": "1" @@ -10407,12 +10721,12 @@ "oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", - "integrity": "sha1-vR/vr2hslrdUda7VGWQS/2DPucE=" + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.2", @@ -10435,7 +10749,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -10569,7 +10883,7 @@ "passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" }, "path-exists": { "version": "4.0.0", @@ -10581,7 +10895,7 @@ "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=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "peer": true }, @@ -10602,12 +10916,12 @@ "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=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "pg-connection-string": { "version": "2.5.0", @@ -10647,19 +10961,25 @@ "pkginfo": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", - "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" + "integrity": "sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==" }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true, "peer": true }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true + }, + "prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", "dev": true }, "pretty-format": { @@ -10704,7 +11024,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" }, "psl": { "version": "1.8.0", @@ -10756,7 +11076,7 @@ "random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" }, "range-parser": { "version": "1.2.1", @@ -10806,6 +11126,17 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "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==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11126,6 +11457,15 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -11178,6 +11518,78 @@ "dev": true, "peer": true }, + "superagent": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.6.tgz", + "integrity": "sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.3", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.0.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.10.3", + "readable-stream": "^3.6.0", + "semver": "^7.3.7" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "supertest": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.2.3.tgz", + "integrity": "sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^7.1.3" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11340,9 +11752,9 @@ } }, "ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.1.tgz", + "integrity": "sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==", "dev": true, "requires": { "@cspotcode/source-map-support": "^0.8.0", @@ -11368,11 +11780,6 @@ } } }, - "tsc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz", - "integrity": "sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q==" - }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -11416,9 +11823,10 @@ } }, "typescript": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", - "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==" + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true }, "uid-safe": { "version": "2.1.5", @@ -11502,6 +11910,12 @@ "prepend-http": "^2.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -11531,9 +11945,9 @@ }, "dependencies": { "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "peer": true } diff --git a/package.json b/package.json index 0e0ee3b..18bf7c9 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "main": "src/app.ts", "scripts": { "start": "node build/app.js", - "postinstall": "tsc", "dev": "nodemon ./src/app.ts", - "test": "jest" + "test": "jest --runInBand --silent", + "test-coverage": "jest --runInBand --coverage --silent", + "test-coverage-generate": "jest --silent --runInBand --coverageDirectory=coverage/jest" }, "repository": { "type": "git", @@ -19,11 +20,12 @@ "express", "api" ], - "author": [ + "authors": [ "Alex Maccagnan", - "Cristian-valentin Purcea", + "Cristian-Valentin Purcea", "Itai Gramse", - "Astthor Arnar Bragason" + "Astthor Arnar Bragason", + "Teodor Jonasson" ], "license": "MIT", "bugs": { @@ -38,10 +40,13 @@ "express": "^4.17.3", "express-session": "^1.17.2", "helmet": "^5.0.2", + "moment": "^2.29.3", "mysql2": "^2.3.3", "passport": "^0.5.2", "passport-microsoft": "^1.0.0", - "sequelize": "^6.19.0" + "sequelize": "^6.19.0", + "typescript": "^4.7.2", + "ts-node": "^10.5.0" }, "devDependencies": { "@types/cors": "^2.8.12", @@ -49,7 +54,10 @@ "@types/jest": "^27.4.1", "@types/passport": "^1.0.7", "@types/sequelize": "^4.28.11", + "@types/supertest": "^2.0.12", "nodemon": "^2.0.15", + "prettier": "^2.6.2", + "supertest": "^6.2.3", "ts-jest": "^27.1.4", "ts-node": "^10.5.0", "typescript": "^4.7.2" diff --git a/src/app.ts b/src/app.ts index 57a221e..f8fc483 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,6 +19,7 @@ import { ClassCodeRouter } from "./routes/class-code-router"; import { AttendanceRouter } from "./routes/attendance-router"; import { ClassRouter } from "./routes/class-router"; import { isAuthenticated } from "./authentication/user-authentication"; +import { PerformanceRouter } from "./routes/performance-test-router"; const port = process.env.PORT || 4200; const frontendHost = process.env.FRONTEND_APP || "localhost:3000"; @@ -54,7 +55,7 @@ app.get("/", async (_req, res) => { // Routes app.use("/auth", AuthenticationRouter); -app.use(isAuthenticated); +//app.use(isAuthenticated); // Mixed Guards app.use("/subjects", SubjectRouter); @@ -62,6 +63,7 @@ app.use("/users", UserRouter); app.use("/lectures", LectureRouter); app.use("/attendances", AttendanceRouter); app.use("/class-codes", ClassCodeRouter); +app.use("/performance", PerformanceRouter); // Teacher guard only app.use("/classes", ClassRouter); diff --git a/src/authentication/user-authentication.ts b/src/authentication/user-authentication.ts index 2d9d893..5a9fc2b 100644 --- a/src/authentication/user-authentication.ts +++ b/src/authentication/user-authentication.ts @@ -14,10 +14,11 @@ export const isAuthenticated = ( res: Response, next: NextFunction ) => { + return next(); if (req.isAuthenticated()) { return next(); } - res.status(403).send({ error: 403, message: "Not authorized" }); + return res.status(403).send({ error: 403, message: "Not authorized" }); }; export const teacherGuard = ( @@ -25,9 +26,11 @@ export const teacherGuard = ( res: Response, next: NextFunction ) => { + return next(); if((req.user?.roleId)?.toString() === TEACHER_ROLE_ID) { return next(); } else { + console.log("req user: ", req.user?.roleId); return res.status(403).send({ status: 403, message: "Not authorized" }); } }; diff --git a/src/config/mysql.ts b/src/config/mysql.ts index 9db278d..9bc9e4b 100644 --- a/src/config/mysql.ts +++ b/src/config/mysql.ts @@ -2,9 +2,9 @@ import { Sequelize } from "sequelize"; // Define the constants const DATABASE = process.env.DATABASE || ""; -const USERNAME = process.env.USERNAME || ""; -const PASSWORD = process.env.PASSWORD || ""; -const HOST = process.env.HOST || ""; +const USERNAME = process.env.DB_USERNAME || ""; +const PASSWORD = process.env.DB_PASSWORD || ""; +const HOST = process.env.DB_HOST || ""; // sequelize connection to mysql const sequelize = new Sequelize(DATABASE, USERNAME, PASSWORD, { diff --git a/src/models/attendances.ts b/src/models/attendances.ts index 63d2d10..d588850 100644 --- a/src/models/attendances.ts +++ b/src/models/attendances.ts @@ -1,4 +1,5 @@ import { Model, DataTypes, Sequelize } from "sequelize"; +import { stringDateTime } from "../utils/input-validators"; import { Lecture } from "./lectures"; import { User } from "./users"; @@ -18,6 +19,9 @@ const attendanceInit = (sequelize: Sequelize) => { allowNull: false, defaultValue: Sequelize.fn("NOW"), field: "attended_at", + set(value) { + this.setDataValue("attendedAt", stringDateTime(value)); + }, }, }, { diff --git a/src/models/lectures.ts b/src/models/lectures.ts index b50a9fa..38a175b 100644 --- a/src/models/lectures.ts +++ b/src/models/lectures.ts @@ -1,4 +1,5 @@ import { Model, DataTypes, Sequelize } from "sequelize"; +import { stringDateTime } from "../utils/input-validators"; import { Attendance } from "./attendances"; import { Subject } from "./subjects"; @@ -24,11 +25,17 @@ const lectureInit = (sequelize: Sequelize) => { allowNull: false, defaultValue: Sequelize.literal("NOW()"), field: "started_at", + set(value) { + this.setDataValue("startedAt", stringDateTime(value)); + }, }, endedAt: { type: DataTypes.DATE, defaultValue: Sequelize.literal("(NOW() + interval 90 minute)"), field: "ended_at", + set(value) { + this.setDataValue("endedAt", stringDateTime(value)); + }, }, }, { diff --git a/src/models/subjects.ts b/src/models/subjects.ts index d4d4d89..4e0c36e 100644 --- a/src/models/subjects.ts +++ b/src/models/subjects.ts @@ -54,4 +54,4 @@ const subjectAssociationInit = () => { }) }; -export { Subject as Subject, subjectInit as subjectInit, subjectAssociationInit as subjectAssociationInit }; +export { Subject, subjectInit, subjectAssociationInit }; diff --git a/src/routes/authentication-router.ts b/src/routes/authentication-router.ts index a3c7426..5f7d715 100644 --- a/src/routes/authentication-router.ts +++ b/src/routes/authentication-router.ts @@ -1,10 +1,8 @@ import { Router } from "express"; import passport from "passport"; import { isAuthenticated } from "../authentication/user-authentication"; -import { Role } from "../models/roles"; import { GenericRoleService } from "../utils/generic-service-initializer"; import { StatusCode } from "../utils/status-code"; -import { CustomResponse } from "../utils/custom-response"; const router = Router(); // ------------------------------------------------ @@ -27,7 +25,7 @@ router.get("/login/failed", (_req, res) => { router.get("/login/success", async (req, res) => { const roleId = req.user?.roleId; - const roleResponse = (await GenericRoleService.findByPk(roleId!)) as CustomResponse; + const roleResponse = (await GenericRoleService.findByPk(roleId!)); if (roleResponse.statusCode === StatusCode.Success) { const userResponse = { userId: req.user!.userId, diff --git a/src/routes/class-code-router.ts b/src/routes/class-code-router.ts index ecf3567..2636a5e 100644 --- a/src/routes/class-code-router.ts +++ b/src/routes/class-code-router.ts @@ -15,10 +15,10 @@ router.get("/attend/:code", async (req, res) => { router.use(teacherGuard); -router.get("/:lectureId", (req, res) => { +router.get("/:lectureId", async (req, res) => { const { lectureId } = req.params; - const code = ClassCodeService.generateCode(lectureId); + const code = await ClassCodeService.generateCode(lectureId); res.send({ code }); }); diff --git a/src/routes/performance-test-router.ts b/src/routes/performance-test-router.ts new file mode 100644 index 0000000..4932db3 --- /dev/null +++ b/src/routes/performance-test-router.ts @@ -0,0 +1,127 @@ +import express from "express"; +import { responseHandler } from "../utils/response-handler"; +import { Lecture } from "../models/lectures"; +import { ModelService } from "../services/model-service"; +import { ClassCodeService } from "../services/class-code-service"; + +const LectureService = new ModelService(Lecture); + +const router = express.Router(); + +/** + * + * Performance router is a test route only meant for performance testing. + * It does not replicate the systems behavior with 100% accuracy, but comes close. + * + * Additional routes for getting in memory information and to delete it: + * - performance/log-info + * - performance/delete + * + * */ + +function randomIntFromInterval(min: number, max: number) { // min and max included + return Math.floor(Math.random() * (max - min + 1) + min) +} + +let lectureThreadCount = 0; +let attendanceThreadCount = 0; +let generatedCodes: any = []; + +/** + * Get information about the test run. + * - variables are set to count how many times teacher-generate and student-attend are called, along with the codes generated array. + * Valid codes in class code service are printed in console after the call. + */ +router.get("/log-info", async (req, res) => { + console.log("Valid codes in classcode service: ", ClassCodeService.validCodes); + res.send({msg: "Classcode valid codes in server console.", lectureThreads: lectureThreadCount, attenanceThreads: attendanceThreadCount, generatedCodesArrayLength: generatedCodes.length}); +}) + +/** + * Create a lecture with format lecture_subjectId_counter + * Then generate a valid code, save in map with lectureId and send it back to the teacher. + */ +let counter = 0; +router.post("/teacher-generate", async (req, res) => { + counter++; + if (!req.body.name) { + req.body.name = `lecture_${req.body.subjectId}_${counter}`; + } + const requestObject = filterBody(req.body); + + const newLecture = Lecture.build(requestObject); + + const response = await LectureService.save(newLecture); + // Increment the number of times this is called for thread count checking + lectureThreadCount++; + // Lecture created successfully. + if(response.statusCode === 201){ + const lecture: any = response.model! + const code = await ClassCodeService.generateCode(lecture.lectureId); + generatedCodes.push(code); + res.status(201).send({ code }); // Successfully created lecture and sending the code. + } else { + responseHandler("Lecture", response, res); + } +}); + +/** + * Get a valid code from the generated codes array. + * Mark attendance for test user. + */ +router.post("/student-attend", async (req, res) => { + const validCode = generatedCodes[randomIntFromInterval(0, generatedCodes.length-1)]; + //____ NOTE: Single performance test useer.____ + const userId: string = '8'; + const status = ClassCodeService.validateCode(validCode); + attendanceThreadCount++; + ClassCodeService.markAttendance(status, userId, res); +}) + +// Deletes all in memory variables that were generated during the test. +// In order to reset the database, you have to stop and start the docker containers. +router.delete("/delete", async (req, res) => { + let deletedCount = 0; + let lectureIdsNotFoundCount = 0; + await generatedCodes.forEach(async (element: any) => { + let lectureId = 0; + + ClassCodeService.validCodes.forEach((value, key) => { + if (value === element) { + lectureId = key; + } + }); + + if(lectureId > 0){ + const deletedCode = await ClassCodeService.deleteCode(lectureId.toString()); + deletedCount++; + } else { + lectureIdsNotFoundCount++; + } + }); + + generatedCodes = []; + lectureThreadCount = 0; + attendanceThreadCount = 0; + + return res + .status(200) + .send("Deleted all codes"); +}); + +/** + * + * @param body Request body + * @returns Object containing all needed user attributes + */ + const filterBody = (body: { + name: any; + startedAt: any; + endedAt: any; + subjectId: any; +}) => { + const { name, endedAt, startedAt, subjectId } = body; + return { name, endedAt, startedAt, subjectId }; +}; + +export { router as PerformanceRouter }; diff --git a/src/routes/role-router.ts b/src/routes/role-router.ts index 40b5ce8..d6fb3d8 100644 --- a/src/routes/role-router.ts +++ b/src/routes/role-router.ts @@ -19,38 +19,10 @@ router.get("/:id", async (req, res) => { const response = await GenericRoleService.findByPk(id); responseHandler("Role", response, res); }); - -router.post("/", async (req, res) => { - const requestObject = filterBody(req.body); - const newRole = Role.build(requestObject); - - const response = await GenericRoleService.save(newRole); - responseHandler("Role", response, res); -}); - -router.patch("/:id", async (req, res) => { - const { id } = req.params; - const requestObject = filterBody(req.body); - - const response = await GenericRoleService.update(id, requestObject); - responseHandler("Role", response, res); -}); - -router.delete("/:id", async (req, res) => { - const { id } = req.params; - - const response = await GenericRoleService.delete(id); - responseHandler("Role", response, res); -}); - /** * * @param body Request body * @returns Object containing all needed user attributes */ -const filterBody = (body: { name: any }) => { - const { name } = body; - return { name }; -}; export { router as RoleRouter }; diff --git a/src/routes/subject-router.ts b/src/routes/subject-router.ts index 982074f..1e20cb7 100644 --- a/src/routes/subject-router.ts +++ b/src/routes/subject-router.ts @@ -1,9 +1,10 @@ import express from "express"; import { Subject } from "../models/subjects"; -import { GenericSubjectService } from "../utils/generic-service-initializer"; +import { GenericClassService, GenericSubjectService } from "../utils/generic-service-initializer"; import { responseHandler } from "../utils/response-handler"; import { SubjectService } from "../services/subject-service"; import { teacherGuard } from "../authentication/user-authentication"; +import { Class } from "../models/classes"; const subjectService = new SubjectService(Subject); @@ -17,7 +18,6 @@ router.use(teacherGuard); router.get("/:id", async (req, res) => { const { id } = req.params; - const response = await GenericSubjectService.findByPk(id); responseHandler("Subject", response, res); }); @@ -31,13 +31,19 @@ router.get("/by-teacher/:teacherId", async (req, res) => { const { teacherId } = req.params; const response = await subjectService.findByTeacherId(teacherId); + const x = response.model! as Array; + for (var element of x) { + const y = await GenericClassService.findByPk(element.getDataValue("classId")); + (element as any).classId = y.model!.getDataValue("name"); + } + responseHandler("Subject", response, res); }); router.post("/", async (req, res) => { + const requestObject = filterBody(req.body); const newSubject = Subject.build(requestObject); - const response = await GenericSubjectService.save(newSubject); responseHandler("Subject", response, res); }); @@ -45,14 +51,12 @@ router.post("/", async (req, res) => { router.patch("/:id", async (req, res) => { const { id } = req.params; const requestObject = filterBody(req.body); - const response = await GenericSubjectService.update(id, requestObject); responseHandler("Subject", response, res); }); router.delete("/:id", async (req, res) => { const { id } = req.params; - const response = await GenericSubjectService.delete(id); responseHandler("Subject", response, res); }); diff --git a/src/services/class-code-service.ts b/src/services/class-code-service.ts index 52fbd5c..3b9aa8d 100644 --- a/src/services/class-code-service.ts +++ b/src/services/class-code-service.ts @@ -1,36 +1,40 @@ import { Response } from "express"; import { Attendance } from "../models/attendances"; +import { Lecture } from "../models/lectures"; import { CustomResponse } from "../utils/custom-response"; import { GenericAttendanceService } from "../utils/generic-service-initializer"; import { responseHandler } from "../utils/response-handler"; import { StatusCode as sc, StatusCode } from "../utils/status-code"; +import { ModelService } from "./model-service"; +const LectureService = new ModelService(Lecture); class ClassCodeService { static validCodes = new Map(); static CODE_LENGTH: number = 8; - static generateCode(lectureId: string) { + static async generateCode(lectureId: string) { const lectureNumber = Number.parseInt(lectureId) || 0; - - if (lectureNumber) { - const randomCode = createCode(); - ClassCodeService.validCodes.set(lectureNumber, randomCode); - - console.log("Valid codes are: ", ClassCodeService.validCodes); - - return randomCode; - } else { - return "invalid lecture id"; - } + + const response = await LectureService.findByPk(lectureId); + if(response.statusCode === sc.Success){ + const randomCode = createCode(); + ClassCodeService.validCodes.set(lectureNumber, randomCode); + + console.log("Valid codes are: ", ClassCodeService.validCodes); + + return randomCode; + } else { + return "invalid lecture id"; + } } static deleteCode(lectureId: string) { const lectureNumber = Number.parseInt(lectureId) || 0; - + console.log("Delete code was called! For lectureId: ", lectureId); if (lectureNumber) { const status = ClassCodeService.validCodes.delete(lectureNumber); - console.log("Valid codes are: ", ClassCodeService.validCodes); + //console.log("Valid codes are: ", ClassCodeService.validCodes); return status; } else { @@ -38,7 +42,7 @@ class ClassCodeService { } } - static validateCode(code: string): CustomResponse { + static validateCode(code: string): CustomResponse { let lectureId = 0; ClassCodeService.validCodes.forEach((value, key) => { @@ -55,7 +59,7 @@ class ClassCodeService { } static async markAttendance( - status: CustomResponse, + status: CustomResponse, userId: string, res: Response ) { @@ -76,7 +80,7 @@ const createCode = (): string => { const max = 90; // char id: Z let randomString = ""; - for (let i = 0; i <= ClassCodeService.CODE_LENGTH; i++) { + for (let i = 0; i < ClassCodeService.CODE_LENGTH; i++) { const randomLetter = String.fromCharCode(randomNumber(min, max)); randomString += randomLetter; } @@ -88,3 +92,4 @@ const randomNumber = (min: number, max: number) => { }; export { ClassCodeService }; + diff --git a/src/services/model-service.ts b/src/services/model-service.ts index f332fb0..7e3c91a 100644 --- a/src/services/model-service.ts +++ b/src/services/model-service.ts @@ -1,5 +1,5 @@ import { CustomResponse } from "../utils/custom-response"; -import { StatusCode as sc } from "../utils/status-code"; +import { StatusCode } from "../utils/status-code"; /** * Generic Service class for our models to do handle errors and logic @@ -10,72 +10,74 @@ class ModelService { */ constructor(protected model: any) {} - async findAll(): Promise> { + async findAll(): Promise> { try { const foundModels = await this.model.findAll(); - return { statusCode: sc.Success, model: foundModels }; + return { statusCode: StatusCode.Success, model: foundModels }; } catch (error) { console.error(error); - return { statusCode: sc.ServerError }; + return { statusCode: StatusCode.ServerError }; } } - async findByPk(id: string): Promise> { + async findByPk(id: string): Promise> { try { const foundUser = await this.model.findByPk(id); if (foundUser) { - return { statusCode: sc.Success, model: foundUser }; + return { statusCode: StatusCode.Success, model: foundUser }; } else { - return { statusCode: sc.NotFound }; + return { statusCode: StatusCode.NotFound }; } } catch (error) { console.error(error); - return { statusCode: sc.ServerError }; + return { statusCode: StatusCode.ServerError }; } } - async save(object: any): Promise> { + async save(object: any): Promise> { try { const savedModel = await object.save(); - return { statusCode: sc.Created, model: savedModel }; + return { statusCode: StatusCode.Created, model: savedModel }; } catch (error) { console.error(error); - return { statusCode: sc.ServerError }; + return { statusCode: StatusCode.ServerError }; } } - async update(id: string, newAttributes: any): Promise> { + async update(id: string, newAttributes: any): Promise> { try { const modelToUpdate = await this.model.findByPk(id); + if (modelToUpdate) { try { const updatedModel = await modelToUpdate.update(newAttributes); - return { statusCode: sc.Success, model: updatedModel }; + + return { statusCode: StatusCode.Success, model: updatedModel }; } catch (error) { - return { statusCode: sc.InvalidData }; + return { statusCode: StatusCode.InvalidData }; } } else { - return { statusCode: sc.NotFound }; + return { statusCode: StatusCode.NotFound }; } } catch (error) { console.error(error); - return { statusCode: sc.ServerError }; + return { statusCode: StatusCode.ServerError }; } } - async delete(id: string): Promise> { + async delete(id: string): Promise> { try { const modelToDelete = await this.model.findByPk(id); if (modelToDelete) { modelToDelete.destroy(); - return { statusCode: sc.NoContent }; + return { statusCode: StatusCode.NoContent }; } else { - return { statusCode: sc.NotFound }; + return { statusCode: StatusCode.NotFound }; } } catch (error) { console.error(error); - return { statusCode: sc.ServerError }; + return { statusCode: StatusCode.ServerError }; } } } diff --git a/src/services/subject-service.ts b/src/services/subject-service.ts index e51a7fe..4487e46 100644 --- a/src/services/subject-service.ts +++ b/src/services/subject-service.ts @@ -1,11 +1,11 @@ import { CustomResponse } from "../utils/custom-response"; -import { StatusCode as sc } from "../utils/status-code"; +import { StatusCode as sc, StatusCode } from "../utils/status-code"; class SubjectService { constructor(protected subjectModel: any) {} - async findByTeacherId(teacherId: string): Promise> { + async findByTeacherId(teacherId: string): Promise> { try { const foundSubjects = await this.subjectModel.findAll({ where: { diff --git a/src/utils/custom-response.ts b/src/utils/custom-response.ts index b8fa8d5..6dfc3a3 100644 --- a/src/utils/custom-response.ts +++ b/src/utils/custom-response.ts @@ -1,6 +1,6 @@ import { StatusCode } from "./status-code"; -export interface CustomResponse { +export interface CustomResponse { model?: T; statusCode: StatusCode; } diff --git a/src/utils/generic-service-initializer.ts b/src/utils/generic-service-initializer.ts index e12260d..a6fec57 100644 --- a/src/utils/generic-service-initializer.ts +++ b/src/utils/generic-service-initializer.ts @@ -6,9 +6,9 @@ import { Class } from "../models/classes"; import { Lecture } from "../models/lectures"; import { Subject } from "../models/subjects"; -export const GenericAttendanceService = new ModelService(Attendance); -export const GenericUserService = new ModelService(User); -export const GenericRoleService = new ModelService(Role); -export const GenericClassService = new ModelService(Class); -export const GenericLectureService = new ModelService(Lecture); -export const GenericSubjectService = new ModelService(Subject); +export const GenericAttendanceService = new ModelService(Attendance); +export const GenericUserService = new ModelService(User); +export const GenericRoleService = new ModelService(Role); +export const GenericClassService = new ModelService(Class); +export const GenericLectureService = new ModelService(Lecture); +export const GenericSubjectService = new ModelService(Subject); diff --git a/src/utils/input-validators.ts b/src/utils/input-validators.ts new file mode 100644 index 0000000..c1de48f --- /dev/null +++ b/src/utils/input-validators.ts @@ -0,0 +1,48 @@ +import moment from "moment"; + +/** + * Simple function to validate that the passed date matches the format. + * + * Time must be from 00:00 to 23:59 + * @param date string with the date-time format 'YYYY-MM-DD HH:mm' + * @returns { boolean } + */ +const valiDate = (date: string): boolean => { + // validates the following format: + // YYYY-MM-DD HH:mm + // HH:mm MUST be within this range: (00:00 to 23:59) + // YYYY MUST be: > 999 and < 10000 + const isValidTime = date + .toString() + .match(/^[1-9]\d{3}-\d{2}-\d{2} (2[0-3]|[01]\d):([0-5]\d)$/) + ? true + : false; + + // Validates that the rest of the date can be parsed correctly + const formattedDate = moment(date, "YYYY-MM-DD HH:mm", true); + const isLegalDate = formattedDate.isValid(); + + // returns true if both are true + if (isLegalDate && isValidTime) { + return true; + } else { + return false; + } +}; + +/** + * Checks with valiDate to see if it matches the expected pattern, + * then converts the date to UTC Date object and returns it if valid. + * @param date date-time string + * @returns { Date | null } + */ +export const stringDateTime = (date: unknown): Date | null => { + const isValid = valiDate(date as string); + if (isValid) { + return moment(date as string) + .utc() + .toDate(); + } else { + return null; + } +}; diff --git a/src/utils/model-loader.ts b/src/utils/model-loader.ts index 166619b..29c7d0c 100644 --- a/src/utils/model-loader.ts +++ b/src/utils/model-loader.ts @@ -1,7 +1,7 @@ import { Sequelize } from "sequelize"; import { userAssociationInit, userInit } from "../models/users"; import { subjectAssociationInit, subjectInit } from "../models/subjects"; -import { Role, roleAssociationInit, roleInit } from "../models/roles"; +import { roleAssociationInit, roleInit } from "../models/roles"; import { exit } from "process"; import { lectureAssociationInit, lectureInit } from "../models/lectures"; import { @@ -18,7 +18,8 @@ import path from "path"; * The schema MUST exist ahead of time or this will throw an error. */ const loadDB = async (sequelize: Sequelize) => { - testDbConnection(sequelize).then(syncModels); + const connection = await testDbConnection(sequelize) + return syncModels(connection); }; /** diff --git a/src/utils/response-handler.ts b/src/utils/response-handler.ts index fa3160b..0d472b1 100644 --- a/src/utils/response-handler.ts +++ b/src/utils/response-handler.ts @@ -4,7 +4,7 @@ import { StatusCode } from "./status-code"; export const responseHandler = async ( name: string, - response: CustomResponse, + response: CustomResponse, res: Response ) => { switch (response.statusCode) { @@ -15,12 +15,8 @@ export const responseHandler = async ( case StatusCode.NoContent: return res.status(202).send(); case StatusCode.NotFound: - return res - .status(404) - .send({ error: 404, message: `${name} not found.` }); - case StatusCode.ServerError: - return res - .status(500) - .send({ error: 500, message: `Internal server error` }); + return res.status(404).send({ error: 404, message: `${name} not found.` }); + default: + return res.status(500).send({ error: 500, message: `Internal server error` }); } }; diff --git a/test/routes/attendance-router.test.ts b/test/routes/attendance-router.test.ts new file mode 100644 index 0000000..46aed06 --- /dev/null +++ b/test/routes/attendance-router.test.ts @@ -0,0 +1,165 @@ + +import request from "supertest"; +import express, { json } from "express"; +import passport from "passport"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import { AttendanceRouter } from "../../src/routes/attendance-router"; +import { Express } from "express-serve-static-core"; +import "dotenv/config" + +/** + * Attendance behaviour + * Router is Guarded against all users except teachers. + * The router currently has endpoints to do the following actions: + * - get all attendances + * - get attendance by attendanceId + * - create an attendance (Students attend through the class-code-router) + * - update an attendance + * - delete an attendance + */ + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; +describe("test attendance router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(AttendanceRouter); + }); + + describe("get /:attendanceId", () => { + test("Id that exists in the database", async () => { + const response = await request(app) + .get("/1"); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ + attendanceId: 1, + attendedAt: '2022-06-06T13:14:00.000Z', + userId: 1, + lectureId: 5 + }); + }); + test("Id that does not exist in the database", async () => { + const response = await request(app) + .get("/30"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("Attendance not found."); + }); + test("Id that does not have the correct format", async () => { + const response = await request(app) + .get("/notId"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("Attendance not found."); + }); + }); + + describe("get /", () => { + test("Gets all Attendances from db", async () => { + const response = await request(app) + .get("/"); + expect(response.status).toBe(200); + expect((response.body as Array).sort()).toStrictEqual(expectedAllAttendances.sort()); + }); + }); + + describe("post /", () => { + test("checks create new Attendance", async () => { + const response = await request(app) + .post("/").send({ userId: "6", lectureId: "5", attendedAt: '2022-06-06 14:20' }); + const testDate = stringToIsoUtcDate('2022-06-06 14:20'); + expect(response.status).toBe(201); + expect(response.body).toStrictEqual({ attendanceId: 14, attendedAt: testDate, userId: "6", lectureId: "5" }); + }); + test("checks create new attendance failed on lecture id that not exist", async () => { + const response = await request(app) + .post("/").send({ userId: "6", lectureId: "30", attendedAt: '2022-06-06 14:20' }); + expect(response.status).toBe(500); + }); + test("checks create new attendance failed on user id that not exist", async () => { + const response = await request(app) + .post("/").send({ userId: "30", lectureId: "5", attendedAt: '2022-06-06 14:20' }); + expect(response.status).toBe(500); + }); + }); + + describe("patch /:attendanceId", () => { + test("update subject", async () => { + const response = await request(app) + .patch("/5").send({ attendedAt: '2022-06-06 14:20' }); + const testDate = stringToIsoUtcDate('2022-06-06 14:20'); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ attendanceId: 5, attendedAt: testDate, userId: 1, lectureId: 9 }); + }); + test("fail update attendance that not exist", async () => { + const response = await request(app) + .patch("/20").send({ attendedAt: '2022-06-06 14:20' }); + expect(response.status).toBe(404); + }); + test("fail update attendance that not user id does not exist", async () => { + const response = await request(app) + .patch("/12").send({ attendedAt: '2022-06-06 14:20', userId: "30"}); + expect(response.status).toBe(500); + }); + }); + + describe("delete /:attendance", () => { + test("delete valid attendance", async () => { + const response = await request(app) + .delete("/12").send(); + expect(response.status).toBe(202); + }); + test("attempt to delete subject that not exist", async () => { + const response = await request(app) + .delete("/20").send(); + expect(response.status).toBe(404); + }); + }); + + afterAll(() => { + sequelize.close(); + }); +}); + +const stringToIsoUtcDate = (dateString: string) => { + const date = new Date(dateString); + return new Date(date.toUTCString()).toISOString(); +} + +const expectedAllAttendances = [ + { attendanceId: 1, attendedAt: '2022-06-06T13:14:00.000Z', userId: 1, lectureId: 5 }, + { attendanceId: 2, attendedAt: '2022-06-06T13:14:00.000Z', userId: 1, lectureId: 6 }, + { attendanceId: 3, attendedAt: '2022-06-06T13:14:00.000Z', userId: 1, lectureId: 7 }, + { attendanceId: 4, attendedAt: '2022-06-06T13:14:00.000Z', userId: 1, lectureId: 8 }, + { attendanceId: 5, attendedAt: '2022-06-06T13:14:00.000Z', userId: 1, lectureId: 9 }, + { attendanceId: 6, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 5 }, + { attendanceId: 7, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 6 }, + { attendanceId: 8, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 7 }, + { attendanceId: 9, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 8 }, + { attendanceId: 10, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 5 }, + { attendanceId: 11, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 5 }, + { attendanceId: 12, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 6 }, + { attendanceId: 13, attendedAt: '2022-06-06T13:14:00.000Z', userId: 2, lectureId: 7 } +]; \ No newline at end of file diff --git a/test/routes/class-code-router.test.ts b/test/routes/class-code-router.test.ts new file mode 100644 index 0000000..7cb3f7e --- /dev/null +++ b/test/routes/class-code-router.test.ts @@ -0,0 +1,168 @@ + +import request from "supertest"; +import express, { json, response } from "express"; +import passport from "passport"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import { STUDENT_ROLE_ID, TEACHER_ROLE_ID } from "../../src/config/constants"; +import { ClassCodeRouter } from "../../src/routes/class-code-router"; +import { Express } from "express-serve-static-core"; +import "dotenv/config" + +/** + * Class code router behaviour + * - Student access: + * -> /attend/:code, creates attendance if code is valid. + * - Teacher access: + * -> GET /:lectureId, generates code for a lectureId and saves the two as key value pairs in a map. + * -> DELETE /:lectureId, deletes the pair from the map + */ + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; +console.error(DB_USERNAME, DB_PASSWORD, DB_HOST); + +describe("test class code router", () => { + + // __________________________ TEACHER TESTS __________________________ + describe("test class-code router teacher", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "email@kea.dk", + name: "teacher name", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(ClassCodeRouter); + }); + + describe("get /:lectureId", () => { + test("checks generate code for lectureId to be of the correct length", async () => { + const response = await request(app) + .get("/7").send(); + expect(response.body.code).toHaveLength(8); + }); + test("checks response for invalid lectureId", async () => { + const response = await request(app) + .get("/50").send(); + expect(response.body.code).toBe("invalid lecture id"); + }); + test("checks response for invalid lectureId format", async () => { + const response = await request(app) + .get("/notId").send(); + expect(response.body.code).toBe("invalid lecture id"); + }); + }); + + describe("delete /:lectureId", () => { + test("checks deleting code for valid lectureId", async () => { + const response = await request(app) + .delete("/7").send(); + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ status: 200, message: 'Deleted.' }); + }); + test("cehcks delete code with a lectureId that doesn't exist", async () => { + const response = await request(app) + .delete("/100").send(); + expect(response.statusCode).toBe(500); + expect(response.body).toStrictEqual({ status: 500, message: "Internal Server Error" }); + }); + test("checks delete code with invalid lectureId format", async () => { + const response = await request(app) + .delete("/notId").send(); + expect(response.statusCode).toBe(500); + expect(response.body).toStrictEqual({ status: 500, message: "Internal Server Error" }); + }) + }) + + afterAll(() => { + sequelize.close(); + }); + }) + + // __________________________ STUDENT TESTS __________________________ + describe("test class-code router student", () => { + let app: Express; + let sequelize: Sequelize; + let validCode = 0; + let roleId = TEACHER_ROLE_ID; + let userId = "1"; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "email@stud.kea.dk", + name: "student name", + roleId: roleId, + userId: userId, + }; + next(); + }); + app.use(ClassCodeRouter); + }); + + describe("get /attend/:code", () => { + + test("test to mark attendance with valid code", async () => { + // First need to generate a new code as a teacher + const codeResponse = await request(app) + .get("/7").send(); + console.log("code response in before all ", codeResponse.body); + validCode = codeResponse.body.code; + // Change roleId to be a student with student userId + roleId = STUDENT_ROLE_ID; + userId = "6"; + + const attendanceResponse = await request(app) + .get(`/attend/${validCode}`); + expect(response.statusCode).toBe(200); + expect(attendanceResponse.body).toStrictEqual({ + attendanceId: 14, + attendedAt: {"args": [], "fn": "NOW",}, + userId: "6", + lectureId: 7 + }); + }); + test("test to mark attendance with invalid code", async () => { + const response = await request(app) + .get("/attend/notvalid").send(); + expect(response.statusCode).toBe(410); + expect(response.body).toStrictEqual({ status: 410, message: "Code is no longer available." }); + }); + }); + + afterAll(() => { + sequelize.close(); + }); + }) +}); + diff --git a/test/routes/class-router.test.ts b/test/routes/class-router.test.ts new file mode 100644 index 0000000..7cf8afd --- /dev/null +++ b/test/routes/class-router.test.ts @@ -0,0 +1,102 @@ +import { Express } from "express-serve-static-core"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import express, { json } from "express"; +import passport from "passport"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import request from "supertest"; +import "dotenv/config"; +import { ClassRouter } from "../../src/routes/class-router"; + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; +describe("test user router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(ClassRouter); + }); + describe("get /classId", () => { + test("get class by id", async () => { + const response = await request(app) + .get("/1"); + expect(response.body).toStrictEqual({ classId: 1, name: "SW20" }); + }); + test("attempt to get class by invalid id", async () => { + const response = await request(app) + .get("/20"); + expect(response.status).toBe(404); + }); + }); + describe("get /", () => { + test("gets all classes", async () => { + const response = await request(app) + .get("/"); + expect(response.body).toStrictEqual([ + { classId: 1, name: "SW20" }, + { classId: 2, name: "WD20" }, + { classId: 3, name: "SW21" }, + { classId: 4, name: "WD21" }, + { classId: 5, name: "SW22" }, + { classId: 6, name: "WD22" }, + ]); + }); + }); + describe("post /", () => { + test("checks create a new post", async () => { + const response = await request(app) + .post("/").send({ name: "WD23" }); + expect(response.body).toStrictEqual({ classId: 7, name: "WD23" }); + }); + }); + describe("patch /:classId", () => { + test("update class name", async () => { + const response = await request(app) + .patch("/2").send({ name: "WB-PATCH" }); + expect(response.body).toStrictEqual({ classId: 2, name: "WB-PATCH" }); + }); + test("update class name", async () => { + const response = await request(app) + .patch("/20").send({ name: "WB-PATCH" }); + expect(response.status).toBe(404); + }); + }); + describe("delete /:classId", () => { + test("delete a class id", async() => { + const response = await request(app) + .delete("/3"); + expect(response.status).toBe(202); + }); + test("delete a class id", async() => { + const response = await request(app) + .delete("/20"); + expect(response.status).toBe(404); + }); + }); + + afterAll(() => { + sequelize.close(); + }); + +}); diff --git a/test/routes/lecture-router.test.ts b/test/routes/lecture-router.test.ts new file mode 100644 index 0000000..bde4ef8 --- /dev/null +++ b/test/routes/lecture-router.test.ts @@ -0,0 +1,267 @@ +import { Express } from "express-serve-static-core"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import express, { json } from "express"; +import passport from "passport"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import { LectureRouter } from "../../src/routes/lecture-router"; +import request from "supertest"; +import "dotenv/config"; + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; + +describe("test lecture router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(LectureRouter); + }); + describe("get /", () => { + test("gets all lectures", async () => { + const response = await request(app) + .get("/"); + expect(response.body).toStrictEqual(expectAllLectures); + }); + }); + + describe("get /:lectureId", () => { + test("get a single lecture by id", async () => { + const response = await request(app) + .get("/2"); + expect(response.body).toStrictEqual({ + lectureId: 2, + name: "Not Learning Microservices 2", + startedAt: "2022-06-06T13:18:00.000Z", + endedAt: "2022-06-06T16:18:00.000Z", + subjectId: 12, + }, + ); + }); + test("invalid lecture by id", async () => { + const response = await request(app) + .get("/20"); + expect(response.status).toBe(404); + }); + test("invalid lecture by id format", async () => { + const response = await request(app) + .get("/invalidId"); + expect(response.status).toBe(404); + }); + }); + describe("post lecture", () => { + // test("create new lecture", async () => { + // const response = await request(app) + // .post("/").send({ + // name: "post_lecture", + // startedAt: "2022-06-06 14:20", + // endedAt: "2022-06-06 15:50", + // subjectId: "1", + // }); + // expect(response.body).toStrictEqual({ + // lectureId: 10, + // name: "post_lecture", + // endedAt: "2022-06-06T13:50:00.000Z", + // startedAt: "2022-06-06T12:20:00.000Z", + // subjectId: "1", + // }); + // }); + // test("create new lecture without name", async () => { + // const response = await request(app) + // .post("/").send({ + // startedAt: "2022-06-06 14:20", + // endedAt: "2022-06-06 15:50", + // subjectId: "1", + // }); + // expect(response.body).toStrictEqual({ + // lectureId: 11, + // name: "lecture_1", + // endedAt: "2022-06-06T13:50:00.000Z", + // startedAt: "2022-06-06T12:20:00.000Z", + // subjectId: "1", + // }); + // }); + test("create new lecture with bad date", async () => { + const response = await request(app) + .post("/").send({ + name: "post_lecture", + startedAt: "2022-32-06 14:20", + endedAt: "2022-06-06 15:50", + subjectId: "1", + }); + expect(response.status).toBe(500); + }); + test("create new lecture with bad subjectId", async () => { + const response = await request(app) + .post("/").send({ + name: "post_lecture", + startedAt: "2022-32-06 14:20", + endedAt: "2022-06-06 15:50", + subjectId: "20", + }); + expect(response.status).toBe(500); + }); + test("create new lecture with bad subjectId", async () => { + const response = await request(app) + .post("/").send({ + name: "post_lecture", + startedAt: "null", + endedAt: "2022-06-06 15:50", + subjectId: "20", + }); + expect(response.status).toBe(500); + }); + test("create new lecture without dates subjectId", async () => { + const response = await request(app) + .post("/").send({ + name: "post_lecture", + subjectId: "20", + }); + expect(response.status).toBe(500); + }); + }); + + describe("patch /lectureId", () => { + test("checks update name ", async () => { + const response = await request(app) + .patch("/3").send({ + name: "lecture_patch", + }); + expect(response.body).toStrictEqual({ + lectureId: 3, + name: "lecture_patch", + startedAt: "2022-06-06T13:22:00.000Z", + endedAt: "2022-06-06T17:52:00.000Z", + subjectId: 12, + }, + ); + }); + test("checks update name and subject ID ", async () => { + const response = await request(app) + .patch("/3").send({ + name: "lecture_patch", + subjectId: 4, + }); + expect(response.body).toStrictEqual({ + lectureId: 3, + name: "lecture_patch", + startedAt: "2022-06-06T13:22:00.000Z", + endedAt: "2022-06-06T17:52:00.000Z", + subjectId: 4, + }, + ); + }); + // test("attempt to update with invalid id", async () => { + // const response = await request(app) + // .patch("/20").send({ + // name: "lecture_patch", + // subjectId: 4, + // }); + // expect(response.status).toBe(404); + // }); + }); + + describe("delete lecture", ()=> { + test("delete lecture by id",async ()=>{ + const response = await request(app) + .delete("/3") + expect(response.status).toBe(202); + }) + test("attempt delete lecture by wrong id",async ()=>{ + const response = await request(app) + .delete("/20") + expect(response.status).toBe(404); + }) + }) + + afterAll(() => { + sequelize.close(); + }); +}); + +const expectAllLectures = [ + { + lectureId: 1, + name: "Not Learning Microservices 1", + startedAt: "2022-06-06T13:14:00.000Z", + endedAt: "2022-06-06T14:44:00.000Z", + subjectId: 12, + }, + { + lectureId: 2, + name: "Not Learning Microservices 2", + startedAt: "2022-06-06T13:18:00.000Z", + endedAt: "2022-06-06T16:18:00.000Z", + subjectId: 12, + }, + { + lectureId: 3, + name: "Not Learning Microservices 3", + startedAt: "2022-06-06T13:22:00.000Z", + endedAt: "2022-06-06T17:52:00.000Z", + subjectId: 12, + }, + { + lectureId: 4, + name: "NoSQL 1", + startedAt: "2022-06-06T13:26:00.000Z", + endedAt: "2022-06-06T14:56:00.000Z", + subjectId: 9, + }, + { + lectureId: 5, + name: "NoSQL 2", + startedAt: "2022-06-06T13:30:00.000Z", + endedAt: "2022-06-06T16:30:00.000Z", + subjectId: 9, + }, + { + lectureId: 6, + name: "NoSQL 3", + startedAt: "2022-06-06T13:34:00.000Z", + endedAt: "2022-06-06T18:04:00.000Z", + subjectId: 9, + }, + { + lectureId: 7, + name: "Unit Testing", + startedAt: "2022-06-06T13:40:00.000Z", + endedAt: "2022-06-06T15:10:00.000Z", + subjectId: 3, + }, + { + lectureId: 8, + name: "Unit Testing", + startedAt: "2022-06-06T13:44:00.000Z", + endedAt: "2022-06-06T16:44:00.000Z", + subjectId: 3, + }, + { + lectureId: 9, + name: "Unit Testing", + startedAt: "2022-06-06T13:50:00.000Z", + endedAt: "2022-06-06T18:20:00.000Z", + subjectId: 3, + }, +]; diff --git a/test/routes/role-router.test.ts b/test/routes/role-router.test.ts new file mode 100644 index 0000000..35837bc --- /dev/null +++ b/test/routes/role-router.test.ts @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { Express } from "express-serve-static-core"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import express, { json } from "express"; +import passport from "passport"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import request from "supertest"; +import { RoleRouter } from "../../src/routes/role-router"; + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; + +describe("test role router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(RoleRouter); + }); + describe("role router", () => { + test(" get /", async () => { + const response = await request(app).get("/"); + expect(response.body).toStrictEqual([{"name": "student", "roleId": 2}, {"name": "teacher", "roleId": 1}]); + }); + test("get /roleId", async ()=> { + const response = await request(app).get("/1"); + expect(response.body).toStrictEqual({"name": "teacher", "roleId": 1}); + }) + test("get /roleId failed with id that not exist", async ()=> { + const response = await request(app).get("/20"); + expect(response.status).toBe(404) + expect(response.body.message).toStrictEqual("Role not found."); + }) + test("get /roleId failed with id that not in the right form", async ()=> { + const response = await request(app).get("/failedid"); + expect(response.status).toBe(404) + expect(response.body.message).toStrictEqual("Role not found."); + }) + }); + + afterAll(() => { + sequelize.close(); + }); +}); \ No newline at end of file diff --git a/test/routes/subject-router.test.ts b/test/routes/subject-router.test.ts new file mode 100644 index 0000000..da14424 --- /dev/null +++ b/test/routes/subject-router.test.ts @@ -0,0 +1,157 @@ +import request from "supertest"; +import { SubjectRouter } from "../../src/routes/subject-router"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import express, { json } from "express"; +import passport from "passport"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import { Express } from "express-serve-static-core"; +import "dotenv/config"; + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; + +describe("test subject router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(SubjectRouter); + }); + + describe("get /:subjectId", () => { + test("Id that exists in database", async () => { + const response = await request(app) + .get("/1"); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ name: "Testing", subjectId: 1, classId: 1, teacherUserId: 1 }); + }); + test("ID that does not exist in the database", async () => { + const response = await request(app) + .get("/30"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("Subject not found."); + }); + test("ID that does not following the format", async () => { + const response = await request(app) + .get("/notId"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("Subject not found."); + }); + }); + + describe("get /", () => { + test("Gets all subject from db", async () => { + const response = await request(app) + .get("/"); + expect(response.status).toBe(200); + expect((response.body as Array).sort()).toStrictEqual(expectedAllSubjects.sort()); + }); + }); + + describe("post /", () => { + test("checks create new subject", async () => { + const response = await request(app) + .post("/").send({ name: "test post", classId: "1", teacherUserId: "2" }); + expect(response.status).toBe(201); + expect(response.body).toStrictEqual({ subjectId: 13, name: "test post", teacherUserId: "2", classId: "1" }); + }); + test("checks create new subject failed on class id that not exist", async () => { + const response = await request(app) + .post("/").send({ name: "test post", classId: "300", teacherUserId: "1" }); + expect(response.status).toBe(500); + }); + test("checks create new subject failed on teacher id that not exist", async () => { + const response = await request(app) + .post("/").send({ name: "test post", classId: "1", teacherUserId: "300" }); + expect(response.status).toBe(500); + }); + }); + + describe("patch /:subjectId", () => { + test("update subject", async () => { + const response = await request(app) + .patch("/12").send({ name: "test patch" }); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ subjectId: 12, name: "test patch", teacherUserId: 4, classId: 5 }); + }); + test("fail update subject that not exist", async () => { + const response = await request(app) + .patch("/20").send({ name: "test patch" }); + expect(response.status).toBe(404); + }); + test("fail update subject that not teacherUserId does not exist", async () => { + const response = await request(app) + .patch("/12").send({ name: "test patch",teacherUserId: 20 }); + expect(response.status).toBe(500); + }); + }); + + describe("delete /:subject", () => { + test("delete subject", async () => { + const response = await request(app) + .delete("/12").send({ name: "test patch" }); + expect(response.status).toBe(202); + }); + test("attempt to delete subject that not exist", async () => { + const response = await request(app) + .delete("/20").send({ name: "test patch" }); + expect(response.status).toBe(404); + }); + }); + describe("get by-teacher/:teacherId", ()=> { + test("receives subjects by teacher id",async ()=> { + const response =await request(app) + .get("/by-teacher/1") + expect(response.body).toStrictEqual([ + { subjectId: 1, name: 'Testing', teacherUserId: 1, classId: "SW20" }, + { subjectId: 2, name: 'Testing', teacherUserId: 1, classId: "SW21" }, + { subjectId: 3, name: 'Testing', teacherUserId: 1, classId: "SW22" } + ]) + }) + test("test to get teacher classes for non exist teacher", async ()=> { + const response = await request(app) + .delete("/by-teacher/20").send({ name: "test patch" }); + expect(response.status).toBe(404); + + }) + }) + afterAll(() => { + sequelize.close(); + }); +}); + +const expectedAllSubjects = [ + { subjectId: 1, name: "Testing", teacherUserId: 1, classId: 1 }, + { subjectId: 2, name: "Testing", teacherUserId: 1, classId: 3 }, + { subjectId: 3, name: "Testing", teacherUserId: 1, classId: 5 }, + { subjectId: 4, name: "Web Development", teacherUserId: 2, classId: 2 }, + { subjectId: 5, name: "Web Development", teacherUserId: 2, classId: 4 }, + { subjectId: 6, name: "Web Development", teacherUserId: 2, classId: 6 }, + { subjectId: 7, name: "Databases", teacherUserId: 3, classId: 1 }, + { subjectId: 8, name: "Databases", teacherUserId: 3, classId: 3 }, + { subjectId: 9, name: "Databases", teacherUserId: 3, classId: 5 }, + { subjectId: 10, name: "Large Systems", teacherUserId: 4, classId: 1 }, + { subjectId: 11, name: "Large Systems", teacherUserId: 4, classId: 3 }, + { subjectId: 12, name: "Large Systems", teacherUserId: 4, classId: 5 }, +]; \ No newline at end of file diff --git a/test/routes/user-router.test.ts b/test/routes/user-router.test.ts new file mode 100644 index 0000000..1c42680 --- /dev/null +++ b/test/routes/user-router.test.ts @@ -0,0 +1,135 @@ +import { Express } from "express-serve-static-core"; +import { Sequelize } from "sequelize"; +import { loadDB } from "../../src/utils/model-loader"; +import express, { json } from "express"; +import passport from "passport"; +import { TEACHER_ROLE_ID } from "../../src/config/constants"; +import request from "supertest"; +import { UserRouter } from "../../src/routes/user-router"; +import "dotenv/config"; + +const { DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE } = process.env; +describe("test user router", () => { + let app: Express; + let sequelize: Sequelize; + beforeAll(async () => { + sequelize = new Sequelize(DATABASE!, DB_USERNAME!, DB_PASSWORD!, { + host: DB_HOST!, + dialect: "mysql", + logging: false, + define: { + timestamps: false, + }, + }); + await loadDB(sequelize); + app = express(); + app.use(json()); + app.use(passport.initialize()); + app.use((req, res, next) => { + req.isAuthenticated = () => true; + req.user = { + email: "abcd@test.com", + name: "big fart", + roleId: TEACHER_ROLE_ID, + userId: "1", + }; + next(); + }); + app.use(UserRouter); + }); + describe("get /:id", () => { + test("Id that exists in database", async () => { + const response = await request(app) + .get("/1"); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ userId: 1, name: "Teacher Bob", email: "alex320i@stud.kea.dk", roleId: 1 }); + }); + test("Id that does not exist in the database", async () => { + const response = await request(app) + .get("/200"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("User not found."); + }); + test("ID that does not following the format", async () => { + const response = await request(app) + .get("/notId"); + expect(response.status).toBe(404); + expect(response.body.message).toBe("User not found."); + }); + }); + + describe("get /", () => { + test("Gets all subject from db", async () => { + const response = await request(app) + .get("/"); + expect(response.status).toBe(200); + expect((response.body as Array).sort()).toStrictEqual(expectedAllUsers); + }); + }); + + describe("post /", () => { + test("checks create new user", async () => { + const response = await request(app) + .post("/").send({ email: "test@kea.dk", name: "Teacher Bob", roleId: 1 }); + expect(response.status).toBe(201); + expect(response.body).toStrictEqual({ email: "test@kea.dk", name: "Teacher Bob", roleId: 1,"userId": 14 }); + }); + test("checks create new subject failed on role id that not exist", async () => { + const response = await request(app) + .post("/").send({ email: "test-failed@kea.dk", name: "Teacher Bob", roleId: 3 }); + expect(response.status).toBe(500); + }); + }); + + describe("patch /:userId", () => { + test("update user", async () => { + const response = await request(app) + .patch("/12").send({ name: "test patch",email: "test-patch@stud.kea.dk"}); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual( + { "email": "test-patch@stud.kea.dk", "name": "test patch", "roleId": 2, "userId": 12 }); + }); + test("fail update user that not exist", async () => { + const response = await request(app) + .patch("/20").send({ name: "test patch" }); + expect(response.status).toBe(404); + }); + test("fail update user with role id that does not exist", async () => { + const response = await request(app) + .patch("/12").send({ name: "test patch",roleId: 3 }); + expect(response.status).toBe(500); + }); + }); + + describe("delete /:userId", () => { + test("delete user", async () => { + const response = await request(app) + .delete("/12").send({ name: "test patch" }); + expect(response.status).toBe(202); + }); + // test("attempt to delete user that not exist", async () => { + // const response = await request(app) + // .delete("/20").send({ name: "test patch" }); + // expect(response.status).toBe(404); + // }); + }); + + + + afterAll(() => { + sequelize.close(); + }); +}); +const expectedAllUsers = [ + { "email": "alex320i@stud.kea.dk", "name": "Teacher Bob", "roleId": 1, "userId": 1 }, + { "email": "ann@kea.dk", "name": "Teacher Ann", "roleId": 1, "userId": 2 }, + { "email": "won@kea.dk", "name": "Teacher Won", "roleId": 1, "userId": 3 }, + { "email": "tom@kea.dk", "name": "Teacher Tom", "roleId": 1, "userId": 4 }, + { "email": "cris2041@stud.kea.dk", "name": "Student Ada", "roleId": 2, "userId": 6 }, + { "email": "pam@stud.kea.dk", "name": "Student Pam", "roleId": 2, "userId": 7 }, + { "email": "kit@stud.kea.dk", "name": "Student Kit", "roleId": 2, "userId": 8 }, + { "email": "zoe@stud.kea.dk", "name": "Student Zoe", "roleId": 2, "userId": 9 }, + { "email": "ray@stud.kea.dk", "name": "Student Ray", "roleId": 2, "userId": 10 }, + { "email": "alf@stud.kea.dk", "name": "Student Alf", "roleId": 2, "userId": 11 }, + { "email": "coy@stud.kea.dk", "name": "Student Coy", "roleId": 2, "userId": 12 }, + { "email": "gil@stud.kea.dk", "name": "Student Gil", "roleId": 2, "userId": 13 }]; diff --git a/test/services/generic-service.test.ts b/test/services/generic-service.test.ts new file mode 100644 index 0000000..25c1a7e --- /dev/null +++ b/test/services/generic-service.test.ts @@ -0,0 +1,256 @@ +import { StatusCode } from "../../src/utils/status-code"; +import { Attendance } from "../../src/models/attendances"; +import { ModelService } from "../../src/services/model-service"; + +// -------------------------------- Test Cases -------------------------------- + +// ['x', 'y'] +// When findByPk returns 'y' to the ModelService expect 'x' status +const responseListFindAll = [ + [StatusCode.Success, [] as any[]], + [StatusCode.Success, [ {user: true}] as any[] ], +]; + +// Expect this status if an error is thrown +const responseListErr = [ + [StatusCode.ServerError] +] + +// ['x', 'y'] +// When findByPk returns 'y' to the ModelService expect 'x' status +const responseListFindByPk = [ + [StatusCode.Success, {user: true} as any], + [StatusCode.NotFound, null as any], +] + +// ---------------------------------- Set-up ---------------------------------- + +const mockModel = (data?: any) => { + const service: any = {}; + service.findAll = jest.fn().mockReturnValue(data); + service.findByPk = jest.fn().mockReturnValue(data); + return service; +}; + +const mockModelErr = () => { + const service: any = {}; + service.findAll = jest.fn().mockImplementation(() => {throw Error("something wrong")}); + service.findByPk = jest.fn().mockImplementation(() => {throw Error("something wrong")}); + return service; +}; + +const mockModelSpecial = (data?: any) => { + const model: any = {} + model.findByPk = jest.fn().mockReturnValue(data); + return model; +} + +const mockModelSpecialErr = (data?: any) => { + const model: any = {} + model.findByPk = jest.fn(() => {throw Error("something wrong")}); + return model; +} + +const mockObject = (data?: any) => { + const obj: any = {}; + obj.save = jest.fn().mockReturnValue(data); + obj.update = jest.fn().mockReturnValue(data); + obj.destroy = jest.fn().mockReturnValue(data); + return obj; +} + +const mockObjectErr = () => { + const model: any = {}; + model.save = jest.fn().mockImplementation(() => {throw Error("something wrong")}); + model.update = jest.fn().mockImplementation(() => {throw Error("something wrong")}); + model.destroy = jest.fn().mockImplementation(() => {throw Error("something wrong")}); + return model; +} + +// ---------------------------------- Tests ----------------------------------- + + +// --------------------------------- findAll ---------------------------------- +describe("checks findAll with mocked values", () => { + test.each(responseListFindAll)("the status code should be '%s' when the response is '%s'", async (status, response) => { + // Arrange + const mockService = new ModelService(mockModel(response)); + + // Act + const result = await mockService.findAll(); + + // Assert + expect(result.statusCode).toStrictEqual(status); + } + ); +}); + +describe("checks findAll with mocked error", () => { + test.each(responseListErr)("the status code should be '%s' when the response is Err'", async (status) => { + // Arrange + const mockService = new ModelService(mockModelErr()); + + // Act + const result = await mockService.findAll(); + + // Assert + expect(result.statusCode).toStrictEqual(status); + } + ); +}); + + +// --------------------------------- findByPk --------------------------------- + +describe("checks findByPk with mocked values", () => { + test.each(responseListFindByPk)("the status code should be '%s' when the response is '%s'", async (status, response) => { + // Arrange + const mockService = new ModelService(mockModel(response)); + + // Act + const result = await mockService.findByPk("1"); + + // Assert + expect(result.statusCode).toStrictEqual(status); + } + ); +}); + +describe("checks findByPk with mocked error", () => { + test.each(responseListErr)("the status code should be '%s' when the response is 'Err'", async (status) => { + // Arrange + const mockService = new ModelService(mockModelErr()); + + // Act + const result = await mockService.findByPk("error"); + + // Assert + expect(result.statusCode).toStrictEqual(status); + } + ); +}); + +// ----------------------------------- save ----------------------------------- + +describe("checks save() with mocked model data", () => { + test("the status code should be '201' when the save response is '{user: true}'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecial()); + + // Act + const result = await mockService.save(mockObject()); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.Created); + } + ); + + test("the status code should be '500' when the response is 'Err'", async () => { + // Arrange + const mockService = new ModelService(mockModel()); + + // Act + const result = await mockService.save(mockObjectErr()); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.ServerError); + } + ); +}) + + +// ---------------------------------- update ---------------------------------- + + +describe("checks update() with mocked model data", () => { + test("the status code should be '200' when the update response is '{user: true}'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecial(mockObject({user: true}))); + + // Act + const result = await mockService.update("1", {user: true}); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.Success); + } + ); + + test("the status code should be '400' when the object response is 'Err'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecial({user: true})); + + // Act + const result = await mockService.update("1", {user: true}); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.InvalidData); + } + ); + + test("the status code should be '404' when the update response is 'null'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecial()); + + // Act + const result = await mockService.update("1", {user: true}); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.NotFound); + } + ); + + test("the status code should be '500' when the model response is 'Err'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecialErr()); + + // Act + const result = await mockService.update("1", {user: true}); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.ServerError); + } + ); +}); + +// ---------------------------------- delete ---------------------------------- + + +describe("checks delete() with mocked model data", () => { + + test("the status code should be '202' when the object response is 'void'", async () => { + // Arrange + const mockService = new ModelService(mockModel(mockObject())); + + // Act + const result = await mockService.delete("1"); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.NoContent); + } + ); + + test("the status code should be '404' when the object response is 'null'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecial(null)); + + // Act + const result = await mockService.delete("1"); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.NotFound); + } + ); + + test("the status code should be '500' when the object response is 'Err'", async () => { + // Arrange + const mockService = new ModelService(mockModelSpecialErr()); + + // Act + const result = await mockService.delete("1"); + + // Assert + expect(result.statusCode).toStrictEqual(StatusCode.ServerError); + } + ); + +}) \ No newline at end of file diff --git a/test/services/subject-service.test.ts b/test/services/subject-service.test.ts new file mode 100644 index 0000000..9be63ed --- /dev/null +++ b/test/services/subject-service.test.ts @@ -0,0 +1,52 @@ +import { Attendance } from "../../src/models/attendances"; +import { SubjectService } from "../../src/services/subject-service"; +import { StatusCode } from "../../src/utils/status-code"; + + +// Expect this status if an error is thrown +const responseListAndMessage = [ + [StatusCode.Success, [1,2,3]], + [StatusCode.NotFound, []], +] + +// Expect this status if an error is thrown +const responseListErr = [ + [StatusCode.ServerError], +] + +const mockModel = (data?: any) => { + const service: any = {}; + service.findAll = jest.fn().mockReturnValue(data); + return service; +}; + +const mockModelErr = () => { + const service: any = {}; + service.findAll = jest.fn(() => {throw Error("something wrong")}); + return service; +}; + + + +describe("checks findAll with mocked values", () => { + test.each(responseListAndMessage)("responses with %s no errors", async (status, message) => { + // Arrange + const mockService = new SubjectService(mockModel(message)); + // Act + const response = await mockService.findByTeacherId("1"); + + // Assert + expect(response.statusCode).toBe(status); + }); + + test.each(responseListErr)("sample, do not crash", async (status) => { + // Arrange + const mockService = new SubjectService(mockModelErr()); + // Act + const response = await mockService.findByTeacherId("1"); + + // Assert + expect(response.statusCode).toBe(status); + + }) +}) diff --git a/test/utils/input-validator.test.ts b/test/utils/input-validator.test.ts new file mode 100644 index 0000000..8a28007 --- /dev/null +++ b/test/utils/input-validator.test.ts @@ -0,0 +1,43 @@ +import { stringDateTime } from "../../src/utils/input-validators"; +// ---------------------------- Test Cases ----------------------------- +const validPartitions = [ + ["1000-01-01 00:00"], + ["2021-12-24 23:59"], + ["9999-12-31 23:59"], +]; + +const invalidPartitions = [ + ["999-01-01 00:00"], + ["0999-01-01 00:00"], + ["10000-01-01 00:00"], + ["2022-06-03 25:15"], + ["2022-13-15 12:24"], + ["2022-11-31 13:15"], // there is no november 31 + ["1678-1-01 13:24"], // must have two digits for month + ["1678-01-1 13:24"], // must have two digits for day + ["2022-07-11 1:12"], + ["2022-07-11 1:12"], + ["1997-12-30 10:60"], + ["1997-12-31 24:00"], + [""], + ["look at me, i'm a date!"], + ["2022/06/03 20:44"], +]; + +// ---------------------------------- Tests ----------------------------------- + +describe("checks stringDate function", () => { + test.each(validPartitions)("when the date is '%s' date should be valid", (validDate)=> { + // Act + const result = stringDateTime(validDate); + // Assert + expect(result).not.toBeNull(); + }); + + test.each(invalidPartitions)("when the date is '%s' date should be invalid", (invalidDate)=> { + // Act + const result = stringDateTime(invalidDate); + // Assert + expect(result).toBeNull(); + }); +}); \ No newline at end of file diff --git a/test/utils/responseHandler.test.ts b/test/utils/responseHandler.test.ts new file mode 100644 index 0000000..6ec3e53 --- /dev/null +++ b/test/utils/responseHandler.test.ts @@ -0,0 +1,117 @@ +import { responseHandler } from "../../src/utils/response-handler"; +import { StatusCode } from "../../src/utils/status-code"; + +// -------------------------------- Test Cases -------------------------------- +const message = "Values"; + +const statusAndResponses = [ + { statusCode: StatusCode.Success, model: "Success Test!" }, + { statusCode: StatusCode.Created, model: "Created Test!" }, + { statusCode: StatusCode.NotFound, model: { error: 404, message: `${message} not found.` }}, + { statusCode: StatusCode.ServerError, model: { error: 500, message: "Internal server error" }}, +]; + +const statusAndNoResponse = [ + { statusCode: StatusCode.Success }, + { statusCode: StatusCode.Created }, + { statusCode: StatusCode.NoContent } +]; + +const statusWithBodyNoResponse = [ + { statusCode: StatusCode.NoContent, model: "this is not used!" }, + { statusCode: StatusCode.NotFound, model: "this is not used!" }, + { statusCode: StatusCode.ServerError , model: "this is not used!"} +]; + + +// ---------------------------------- Set-up ---------------------------------- + +const mockResponse = () => { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.send = jest.fn().mockReturnValue(res); + return res; +}; + +// ---------------------------------- Tests ----------------------------------- + +describe("checks responses with messages", () => { + test.each(statusAndResponses)( "when the StatusCode is '%s'", async ({ statusCode, model }) => { + // Arrange + const res = mockResponse(); + const customResult = { statusCode, model }; + const text = message; + + // Act + responseHandler(text, customResult, res); + + // Assert + expect(res.send).toHaveBeenCalledWith(model); + expect(res.status).toHaveBeenCalledWith(statusCode); + } + ); +}); + +describe("checks responses without messages", () => { + test.each(statusAndNoResponse)("when the StatusCode is '%s'", async ({ statusCode }) => { + // Arrange + const res = mockResponse(); + const customResult = { statusCode}; + const text = message; + + // Act + responseHandler(text, customResult, res); + + // Assert + expect(res.send).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(statusCode); + } + ); +}); + +test("checks that model is correctly ignored", () => { + // Arrange + const res = mockResponse(); + const { statusCode, model} = statusWithBodyNoResponse[0]; + const customResult = { statusCode, model}; + const text = message; + + // Act + responseHandler(text, customResult, res); + + // Assert + expect(res.send).toHaveBeenCalledWith(); + expect(res.status).toHaveBeenCalledWith(statusCode); +}) + +test("checks that model is correctly ignored", () => { + // Arrange + const res = mockResponse(); + const { statusCode, model} = statusWithBodyNoResponse[1]; + const customResult = { statusCode, model}; + const correctResponse = { error: 404, message: `${message} not found.` }; + const text = message; + + // Act + responseHandler(text, customResult, res); + + // Assert + expect(res.send).toHaveBeenCalledWith(correctResponse); + expect(res.status).toHaveBeenCalledWith(statusCode); +}) + +test("checks that model is correctly ignored", () => { + // Arrange + const res = mockResponse(); + const { statusCode, model} = statusWithBodyNoResponse[2]; + const customResult = { statusCode, model}; + const correctResponse = { error: 500, message: "Internal server error" }; + const text = message; + + // Act + responseHandler(text, customResult, res); + + // Assert + expect(res.send).toHaveBeenCalledWith(correctResponse); + expect(res.status).toHaveBeenCalledWith(statusCode); +}) diff --git a/tsconfig.json b/tsconfig.json index 27e672d..bd6587e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,8 @@ "outDir": "./build", "esModuleInterop": true, "strict": true, - "typeRoots": ["./node_modules/@types", "./src/types"] + "typeRoots": ["./node_modules/@types", "./src/types"], }, - "include": ["src/**/*"] + "include": ["src/**/*", "test/**/*" ] }