diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 48efc8f..8c440b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,12 +31,10 @@ jobs: env: NODE_ENV: ${{ secrets.NODE_ENV }} - SECRET_ADMIN_API: ${{ secrets.SECRET_ADMIN_API }} + SECRET_ADMIN_KEY: ${{ secrets.SECRET_ADMIN_KEY }} SECRET_EMAIL_RECEIVER: ${{ secrets.SECRET_EMAIL_RECEIVER }} SECRET_EMAIL_SENDER: ${{ secrets.SECRET_EMAIL_SENDER }} SECRET_EMAIL_PASS: ${{ secrets.SECRET_EMAIL_PASS }} - SECRET_PRIVATE_KEY_ARTDV: none - SECRET_PRIVATE_KEY_TAVA: none SECRET_BETTERSTACK_LOGGING_KEY: ${{ secrets.SECRET_BETTERSTACK_LOGGING_KEY }} SECRET_BETTERSTACK_HOST: ${{ secrets.SECRET_BETTERSTACK_HOST }} SECRET_DB_USER: ${{ secrets.SECRET_DB_USER }} diff --git a/README.md b/README.md index 972a775..1cd7849 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,81 @@ # yqni13 | support -$\texttt{\color{teal}{v1.4.1}}$ +$\texttt{\color{teal}{v1.4.4}}$ +### Support hub - handling feedback & ratings (`/feedback`) and bug/support requests (`/tickets`) including file attachments across multiple applications via REST API built with NodeJS (Typescript), Express & PostgreSQL in Docker container. Created following Test-Driven Development (450+ tests including ephemeral database by testcontainers) and hosting env:prod via Render, Neon and Cloudflare.
-
- logo +
+ NodeJS + Express + Jest + Neon + Docker + Jenkins + PostgreSQL + Cloudflare + Betterstack + Testcontainers
-### Technology - -
- NodeJS - Express - Jest - Neon -
-
- Docker - Jenkins - PostgreSQL -
-
- Cloudflare - Betterstack - Testcontainers -
+

-
+## 🪄 $\textsf{\color{salmon}Getting started}$ -## How to -### Build & Deploy -This application server will is hosted by Render in a Docker container and a PostgreSQL database hosted by Neon. Additionally a cron-job is set up to keep the service alive on Render due to 15-min inactivity on free tier plan.
-The development process is structured by the TDD (test driven development) principle. +### $\textsf{\color{teal}Prerequisites}$ +- node: v22+ +- PostgreSQL v17+ (local or hosted like Neon) +- Docker v4.54+ +- Cloudflare R2 bucket (file handling) +- Betterstack Telemetry (logging)
-## Overview - -### $\textsf{\color{teal}Features}$ - -
-
🪲 support/bug/feedback-ticket handling including client + user data
-
✨ counting/adding up ratings and administer rating average
-
📂 file handling (upload/delete) from requests + cloud storage
-
:mag: filtered search for ticket + user data (properties + timespan)
-
:closed_lock_with_key: en/disable application (maintenance mode) triggered by request/logic
-
:key: request verification by api-keys
-
🕵️ request rate limiting + violation handling
-
+### $\textsf{\color{teal}Local setup}$ +Download or clone project +```sh +git clone https://github.com/yqni13/support +``` +Create new .env file and fill in your credentials/other env data [(see docs)](./docs/CONFIGURATION.md).
+Navigate/cd into project directory ./backend and install dependencies via npm +```sh +npm ci +``` +Run migrations [(see docs)](./docs/MIGRATION.md).
+Start application in local (development) environment: +```sh +npm run start:dev +``` +Alternatively run application in Docker container [(see docs)](./docs/DEVOPS.md).
-### $\textsf{\color{teal}Tickets}$ - -Documentation follows with finished refactoring (task: SUPPORT-65). +## 🧩 $\textsf{\color{salmon}Features}$ +| Feature | Description | +|---------|-------------| +| 🪲 Ticket system | Handles support & bug reports per client with status lifecycle and optional file attachments | +| ✨ Feedback & Rating system | Abuse-resistant rating system with atomic aggregate updates - one active rating per user per client | +| 📂 Cloud file handling | Upload/delete via Cloudflare R2 (S3-compatible) - supporting pdf & images up to 1MB each, max 5 per ticket | +| 🔎 Filtered search | Query ticket and user data by properties and/or timespan | +| 🔐 Maintenance Mode | Enable/disable application triggered by request or internal logic | +| 🕵️ Rate limiting | Request throttling with violation handling | +| 🔑 API Key Auth | Client authentication via API keys |
-### $\textsf{\color{teal}Feedback/Rating}$ +### $\textsf{\color{teal}Feedback \&\ Rating}$ -Documentation follows with finished refactoring (task: SUPPORT-65). +User can utilize a feedback & rating system to rate the application in use and send criticism or praise. For every client can exist multiple entries for the entity `Feedback` but only one `FeedbackRating` which holds the accumulated data of the pointing feedback entries.
+Resubmissions are handled in the database by an `ON CONFLICT` upsert query [see upsertInTa()](./backend/src/repositories/feedback.repository.ts) on the unique `(client_id, user_id)` constraint, followed by an atomic aggregate update to the 'FeedbackRating' table entry. Both queries are executed within a single transaction to guarantee data consistency.
+The rating happens numerical (1-5) and returns an average rating value as number with up to 1 decimal place.
+Furthermore, if an existing feedback entry has a message stored, but is not reviewed, the feedback gets NOT updated and request throws a specific exception.
### $\textsf{\color{teal}File handling}$ -User can attach files for any support/bug ticket to provide further information (screenshots, images, ...) on their message. Attachments are limited to upload up to `5` files and each file can be up to `1`mb [see validation](./backend/src/middleware/files/validate.files.middleware.ts). Currently only `images` (webp, jpg, jpeg, png) and `pdf` files are supported, but more will follow. Cloud in use is `Cloudflare` (see Figure 1) using S3Client for api communication and files will be deleted when a ticket is closed, canceled or expired (time check). +User can attach files for any support/bug ticket to provide further information (screenshots, images, ...) on their message. Attachments are limited to upload up to `5` files and each file can be up to `1`MB [see validation](./backend/src/middleware/files/validate.files.middleware.ts). Currently only `images` (webp, jpg, jpeg, png) and `pdf` files are supported, but more will follow. Cloud in use is `Cloudflare` (see Figure 1) using S3Client for api communication and files will be deleted when a ticket is closed, canceled or expired (time check).
 Cloudflare upload demo Figure 1 - Cloudflare upload demo, v1.0.0 @@ -75,7 +83,7 @@ User can attach files for any support/bug ticket to provide further information
-### $\textsf{\color{teal}Logging}$ +## 📝 $\textsf{\color{salmon}Logging}$ To monitor errors the logging framework `Winston` is used in combination with Logtail from `Betterstack` as a Singleton: [config](./backend/src/logger/config.logger.ts)
While working within local (DEV) or test environment, error messages are logged into the consoles. For the deployed environments (STAG/PROD) the logging is set to send logtails to Betterstack (longer storage time than app-hosting service). For easy access and monitoring of error messages, the Betterstack UI client dashboard comes in handy (see Figure 2). Additional meta data (environment + version numbers) help identifying and assigning errors. @@ -86,7 +94,7 @@ To monitor errors the logging framework `Winston` is used in combination with Lo
-## Testing +## 🔧 $\textsf{\color{salmon}Testing}$ ### $\textsf{\color{teal}Demo}$ @@ -125,7 +133,8 @@ Install the packages `@jest/globals`, `@types/jest`, `supertest`, `@testcontaine ```sh npm install jest @jest/globals @types/jest supertest @testcontainers/postgresql testcontainers --save-dev ``` -450+ tests exist currently for models, utils, validators and workflows (integration tests) - see [tests](./backend/tests).
+`450+ tests` exist currently for models, utils, validators and workflows (integration tests) - [see tests](./backend/tests).
+Integration-Tests can only run with `active Docker service` due to the ephemeral (temporary) database by testcontainers.
Run tests on local device by including setup for dotenv/config to provide environment variables: ```sh set NODE_ENV=test && jest --setupFiles dotenv/config @@ -155,22 +164,22 @@ Preventing an unwanted merge with unfinished/failed test run, the project is set
-## Updates +## 📈 $\textsf{\color{salmon}Updates}$ [see changelog for all updates](/docs/CHANGELOG.md) -### $\textsf{\color{forestgreen}last update:}$ -$\textsf{[v1.3.5\ =>\ {\textbf{\color{brown}v1.4.1}]}}$ app
-$\textsf{[v1.5.4\ =>\ {\textbf{\color{brown}v1.6.0}]}}$ database -- $\textsf{\color{teal}Addition:}$ Added api route + logic for Feedback/FeedbackRating to add new feedback/rating or get current average rating value. +$\textsf{[v1.4.1\ =>\ {\textbf{\color{brown}v1.4.4}]}}$ app
- $\textsf{\color{orange}Patch:}$ Updated: - + testing whole process (request-to-response) with individual injection of middlewares when necessary. - + database with new migration to add tables 'feedback_entries' and 'feedback_ratings' to handle single feedback/ratings seperately from accumulated average rating. + + entity ID's are using now nominal types instead basic string|number. + + some model functions are renamed to keep consistency and improve readability. + + some api routes have been shortened to keep consistency and improve readability.
### Update objectives:
+
- caching layer
+
- background worker
- jenkins setup
- mail setup
\ No newline at end of file diff --git a/assets/img/logo.png b/assets/img/logo.png deleted file mode 100644 index 760b6c9..0000000 Binary files a/assets/img/logo.png and /dev/null differ diff --git a/assets/img/readme-bg.png b/assets/img/readme-bg.png deleted file mode 100644 index 22cc550..0000000 Binary files a/assets/img/readme-bg.png and /dev/null differ diff --git a/backend/package-lock.json b/backend/package-lock.json index ab057c9..10d1d07 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "support_backend", - "version": "1.4.1", + "version": "1.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "support_backend", - "version": "1.2.1", + "version": "1.4.4", "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.985.0", @@ -935,13 +935,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", - "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.3.4", + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, "engines": { @@ -1620,27 +1620,6 @@ "node": ">=6" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2851,9 +2830,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4116,13 +4095,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -5920,10 +5899,25 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.0.tgz", + "integrity": "sha512-7mtITW/we2/wTUZqMyBOR2F8xP4CRxMiSEcQxPIqdRWdO2L/HZSOlzoNyghmyDwNB8BDxePooV1ZTJpkOUhdRg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.2" + } + }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", "funding": [ { "type": "github", @@ -5932,7 +5926,8 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -6057,9 +6052,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6288,16 +6283,37 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7777,12 +7793,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -7795,6 +7811,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7836,21 +7853,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/multer/node_modules/media-typer": { @@ -7883,18 +7901,6 @@ "node": ">= 0.6" } }, - "node_modules/multer/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/multer/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7990,6 +7996,29 @@ } } }, + "node_modules/node-pg-migrate/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/node-pg-migrate/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/node-pg-migrate/node_modules/glob": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", @@ -8015,16 +8044,16 @@ } }, "node_modules/node-pg-migrate/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8085,9 +8114,9 @@ } }, "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -8286,6 +8315,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.2.tgz", + "integrity": "sha512-LXWqJmcpp2BKOEmgt4CyuESFmBfPuhJlAHKJsFzuJU6CxErWk75BrO+Ni77M9OxHN6dCYKM4vj+21Z6cOL96YQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8658,9 +8702,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8757,9 +8801,9 @@ } }, "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9413,9 +9457,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", @@ -9576,9 +9620,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { diff --git a/backend/package.json b/backend/package.json index 61b0909..7980648 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "support_backend", - "version": "1.4.1", + "version": "1.4.4", "appMeta": { "db_version": "1.6.0", "docker_image": "yqni13/support", diff --git a/backend/src/configs/config.ts b/backend/src/configs/config.ts index 528e618..e21a57b 100644 --- a/backend/src/configs/config.ts +++ b/backend/src/configs/config.ts @@ -1,12 +1,10 @@ export const Config: any = { - ADMIN_API: process.env.SECRET_ADMIN_API || null, + ADMIN_KEY: process.env.SECRET_ADMIN_KEY || null, ENV_MODE: process.env.NODE_ENV || 'development', PORT: process.env.ENV_PORT || 3000, EMAIL_RECEIVER: process.env.SECRET_EMAIL_RECEIVER || null, EMAIL_SENDER: process.env.SECRET_EMAIL_SENDER || null, EMAIL_PASS: process.env.SECRET_EMAIL_PASS || null, - PRIVATE_KEY_ARTDV: process.env.SECRET_PRIVATE_KEY_ARTDV || null, - PRIVATE_KEY_TAVA: process.env.SECRET_PRIVATE_KEY_TAVA || null, BETTERSTACK_LOGGING_KEY: process.env.SECRET_BETTERSTACK_LOGGING_KEY || null, BETTERSTACK_HOST: process.env.SECRET_BETTERSTACK_HOST || null, DB_USER: process.env.SECRET_DB_USER || null, @@ -14,19 +12,19 @@ export const Config: any = { DB_HOST: process.env.SECRET_DB_HOST || null, DB_PORT: process.env.SECRET_DB_PORT || null, DB_DATABASE: process.env.SECRET_DB_DATABASE || null, - DB_TEST_USER: process.env.SECRET_DB_TEST_USER || null, - DB_TEST_PASS: process.env.SECRET_DB_TEST_PASS || null, - DB_TEST_HOST: process.env.SECRET_DB_TEST_HOST || null, - DB_TEST_PORT: process.env.SECRET_DB_TEST_PORT || null, - DB_TEST_DATABASE: process.env.SECRET_DB_TEST_DATABASE || null, + DB_TEST_USER: process.env.SECRET_DB_TEST_USER || 'testuser', + DB_TEST_PASS: process.env.SECRET_DB_TEST_PASS || 'testpass', + DB_TEST_HOST: process.env.SECRET_DB_TEST_HOST || 'localhost', + DB_TEST_PORT: process.env.SECRET_DB_TEST_PORT || 5432, + DB_TEST_DATABASE: process.env.SECRET_DB_TEST_DATABASE || 'testdb', TEST_APIKEY_RAW: process.env.SECRET_TEST_APIKEY_RAW || null, TEST_APIKEY_HASH: process.env.SECRET_TEST_APIKEY_HASH || null, - RATELIMITS_CLIENTSBURSTLIMIT: process.env.SECRET_RATELIMITS_CLIENTSBURSTLIMIT || null, - RATELIMITS_CLIENTSDAILYLIMIT: process.env.SECRET_RATELIMITS_CLIENTSDAILYLIMIT || null, - RATELIMITS_USERSBURSTLIMIT: process.env.SECRET_RATELIMITS_USERSBURSTLIMIT || null, - RATELIMITS_USERSDAILYLIMIT: process.env.SECRET_RATELIMITS_USERSDAILYLIMIT || null, - RATELIMITS_TOTALDAILYLIMIT: process.env.SECRET_RATELIMITS_TOTALDAILYLIMIT || null, - DEMOLIMITS_TOTALDAILYLIMIT: process.env.SECRET_DEMOLIMITS_TOTALDAILYLIMIT || null, + RATELIMITS_CLIENTSBURSTLIMIT: process.env.SECRET_RATELIMITS_CLIENTSBURSTLIMIT || 99, + RATELIMITS_CLIENTSDAILYLIMIT: process.env.SECRET_RATELIMITS_CLIENTSDAILYLIMIT || 99, + RATELIMITS_USERSBURSTLIMIT: process.env.SECRET_RATELIMITS_USERSBURSTLIMIT || 99, + RATELIMITS_USERSDAILYLIMIT: process.env.SECRET_RATELIMITS_USERSDAILYLIMIT || 99, + RATELIMITS_TOTALDAILYLIMIT: process.env.SECRET_RATELIMITS_TOTALDAILYLIMIT || 99, + DEMOLIMITS_TOTALDAILYLIMIT: process.env.SECRET_DEMOLIMITS_TOTALDAILYLIMIT || 20, CLOUD_BUCKET: process.env.SECRET_CLOUD_BUCKET || null, CLOUD_ENDPOINT: process.env.SECRET_CLOUD_ENDPOINT || null, CLOUD_ACCESS_KEY_ID: process.env.SECRET_CLOUD_ACCESS_KEY_ID || null, diff --git a/backend/src/configs/db.ts b/backend/src/configs/db.ts index 1449b69..499256b 100644 --- a/backend/src/configs/db.ts +++ b/backend/src/configs/db.ts @@ -16,7 +16,7 @@ export class DBConnection { #pool: pg.Pool; constructor() { - const connectionString = this._getConnectionString(secrets.ENV_MODE); + const connectionString = this.getConnectionString(secrets.ENV_MODE); this.#pool = new pg.Pool({connectionString}); } @@ -27,7 +27,7 @@ export class DBConnection { return DBConnection.instance; } - _getConnectionString(env: string) { + private getConnectionString(env: string) { // Remove white spaces to be comparable with enum values. env = env.trim() as EnvMode; let db: string; diff --git a/backend/src/controllers/clients.controller.ts b/backend/src/controllers/clients.controller.ts index 11dd7c9..17be71e 100644 --- a/backend/src/controllers/clients.controller.ts +++ b/backend/src/controllers/clients.controller.ts @@ -7,6 +7,7 @@ import { ClientsStatusUpdateDTO } from "../dtos/clients.dto"; import clientsService from "../services/clients.service"; +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; class ClientsController { async getClientStatus(req: Request, res: Response, next: NextFunction) { @@ -34,7 +35,7 @@ class ClientsController { async patchClientStatus(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: string = req.params.id; + const id = req.params.id as ClientsId; const dto: ClientsStatusUpdateDTO = req.body; const response: ClientsStatusResponseDTO | null = await clientsService.updateClientStatus(id, dto); res.json(response); diff --git a/backend/src/controllers/feedback-rating.controller.ts b/backend/src/controllers/feedback-rating.controller.ts index ac199f8..8cb13b1 100644 --- a/backend/src/controllers/feedback-rating.controller.ts +++ b/backend/src/controllers/feedback-rating.controller.ts @@ -2,12 +2,13 @@ import { NextFunction, Request, Response } from "express"; import { checkValidation } from "../middleware/validation.middleware"; import feedbackRatingService from "../services/feedback-rating.service"; import { FeedbackRatingExtendedResponseDTO, FeedbackRatingResponseDTO } from "../dtos/feedback-rating.dto"; +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; class FeedbackRatingController { async getExtendedFeedbackRating(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = req.params.id; + const id = req.params.id as ClientsId; const response: FeedbackRatingExtendedResponseDTO | null = await feedbackRatingService.getExtendedFeedbackRatingById(id); res.json(response); } catch(err: any) { diff --git a/backend/src/controllers/feedback.controller.ts b/backend/src/controllers/feedback.controller.ts index f370374..a55595b 100644 --- a/backend/src/controllers/feedback.controller.ts +++ b/backend/src/controllers/feedback.controller.ts @@ -2,12 +2,13 @@ import { NextFunction, Request, Response } from "express"; import { checkValidation } from "../middleware/validation.middleware"; import { FeedbackCreateDTO, FeedbackFilterDTO, FeedbackResponseDTO } from "../dtos/feedback.dto"; import feedbackService from "../services/feedback.service"; +import { FeedbackId } from "../repositories/interfaces/feedback.entity.interface"; class FeedbackController { async getFeedback(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = +req.params.id; + const id = (+req.params.id) as FeedbackId; const response: FeedbackResponseDTO | null = await feedbackService.getFeedbackById(id); res.json(response); } catch(err: any) { @@ -44,7 +45,7 @@ class FeedbackController { async patchFeedbackReview(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = +req.params.id; + const id = (+req.params.id) as FeedbackId; const response: FeedbackResponseDTO | null = await feedbackService.updateFeedbackReview(id); res.json(response); } catch(err: any) { diff --git a/backend/src/controllers/meta.controller.ts b/backend/src/controllers/meta.controller.ts index 6ee6b94..a44a984 100644 --- a/backend/src/controllers/meta.controller.ts +++ b/backend/src/controllers/meta.controller.ts @@ -2,12 +2,13 @@ import { NextFunction, Request, Response } from "express"; import metaService from "../services/meta.service"; import { checkValidation } from "../middleware/validation.middleware"; import { MaintenanceResponseDTO, MaintenanceUpdateDTO, MetaResponseDTO, MetaUpdateDTO } from "../dtos/meta.dto"; +import { MetaId } from "../repositories/interfaces/meta.entity.interface"; class MetaController { async getMetaById(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: number = +(req.params.id); + const id = (+req.params.id) as MetaId; const response: MetaResponseDTO | null = await metaService.getMetaById(id); res.json(response); } catch(err: any) { @@ -38,7 +39,7 @@ class MetaController { async patchMeta(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: number = +(req.params.id); + const id = (+req.params.id) as MetaId; const dto: MetaUpdateDTO = req.body; const response: MetaResponseDTO | null = await metaService.updateMeta(id, dto); res.json(response); @@ -61,7 +62,7 @@ class MetaController { async patchMaintenanceMode(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: number = +req.params.id; + const id = (+req.params.id) as MetaId; const dto: MaintenanceUpdateDTO = req.body; const response: MaintenanceResponseDTO | null = await metaService.updateMaintenanceMode(id, dto); res.json(response); diff --git a/backend/src/controllers/tickets.controller.ts b/backend/src/controllers/tickets.controller.ts index 602ff7e..8f34398 100644 --- a/backend/src/controllers/tickets.controller.ts +++ b/backend/src/controllers/tickets.controller.ts @@ -8,12 +8,13 @@ import { TicketsUpdateDTO } from "../dtos/tickets.dto"; import ticketsService from "../services/tickets.service"; +import { TicketsId } from "../repositories/interfaces/tickets.entity.interface"; class TicketsController { async getTicket(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = req.params.id; + const id = req.params.id as TicketsId; const response: TicketsResponseExtendedDTO | null = await ticketsService.getTicketById(id); res.json(response); } catch(err: any) { @@ -65,7 +66,7 @@ class TicketsController { async patchTicket(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: string = req.params.id; + const id = req.params.id as TicketsId; const dto: TicketsUpdateDTO = req.body; const response: TicketsResponseDTO | null = await ticketsService.updateTicket(id, dto); res.json(response); @@ -77,7 +78,7 @@ class TicketsController { async deleteTicket(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = req.params.id; + const id = req.params.id as TicketsId; const response: boolean = await ticketsService.deleteTicket(id); res.json(response); } catch(err: any) { diff --git a/backend/src/controllers/users.controller.ts b/backend/src/controllers/users.controller.ts index 23d9dde..9902242 100644 --- a/backend/src/controllers/users.controller.ts +++ b/backend/src/controllers/users.controller.ts @@ -2,12 +2,13 @@ import { NextFunction, Request, Response } from "express"; import { UsersUpdateDTO, UsersFilterDTO, UsersResponseDTO, UsersCreateDTO } from "../dtos/users.dto"; import { checkValidation } from "../middleware/validation.middleware"; import usersService from "../services/users.service"; +import { UsersId } from "../repositories/interfaces/users.entity.interface"; class UsersController { async getUserById(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id = req.params.id; + const id = req.params.id as UsersId; const response: UsersResponseDTO | null = await usersService.getUserById(id); res.json(response); } catch(err: any) { @@ -65,7 +66,7 @@ class UsersController { async patchUser(req: Request, res: Response, next: NextFunction) { try { checkValidation(req); - const id: string = req.params.id; + const id = req.params.id as UsersId; const dto: UsersUpdateDTO = req.body; const response: UsersResponseDTO | null = await usersService.updateUser(id, dto); res.json(response); diff --git a/backend/src/dtos/clients.dto.ts b/backend/src/dtos/clients.dto.ts index e72ce5c..ab95c0a 100644 --- a/backend/src/dtos/clients.dto.ts +++ b/backend/src/dtos/clients.dto.ts @@ -1,3 +1,4 @@ +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; import { ApiKeyStatus } from "../utils/enums/api-key-status.enum"; import { Flag } from "../utils/enums/flag.enum"; @@ -20,7 +21,7 @@ export interface ClientsLastUseUpdateDTO { } export interface ClientsExistResponseDTO { - client_id: string, + client_id: ClientsId, name: string, api_key_hash: string, status: ApiKeyStatus, @@ -31,7 +32,7 @@ export interface ClientsExistResponseDTO { } export interface ClientsCreateResponseDTO { - client_id: string, + client_id: ClientsId, name: string, api_key: string, status: ApiKeyStatus, @@ -42,7 +43,7 @@ export interface ClientsCreateResponseDTO { } export interface ClientsFlagResponseDTO { - client_id: string, + client_id: ClientsId, flag: Flag | null, last_use: string, last_modified: string, @@ -50,7 +51,7 @@ export interface ClientsFlagResponseDTO { } export interface ClientsStatusResponseDTO { - client_id: string, + client_id: ClientsId, name: string, status: ApiKeyStatus, last_use: string, @@ -59,7 +60,7 @@ export interface ClientsStatusResponseDTO { } export interface ClientsLastUseResponseDTO { - client_id: string, + client_id: ClientsId, name: string, last_use: string, last_modified: string, diff --git a/backend/src/dtos/demo-limits.dto.ts b/backend/src/dtos/demo-limits.dto.ts index 429a291..7f458b4 100644 --- a/backend/src/dtos/demo-limits.dto.ts +++ b/backend/src/dtos/demo-limits.dto.ts @@ -1,3 +1,5 @@ +import { DemoLimitsId } from "../repositories/interfaces/demo-limits.entity.interface" + export interface DemoLimitsCountDTO { day: string } @@ -8,7 +10,7 @@ export interface DemoLimitsUpdateDTO { } export interface DemoLimitsResponseDTO { - demo_limit_id: number, + demo_limit_id: DemoLimitsId, day: string, count: number, last_modified: string diff --git a/backend/src/dtos/feedback-rating.dto.ts b/backend/src/dtos/feedback-rating.dto.ts index 4408a4a..b9e638b 100644 --- a/backend/src/dtos/feedback-rating.dto.ts +++ b/backend/src/dtos/feedback-rating.dto.ts @@ -1,5 +1,7 @@ +import { ClientsId } from "../repositories/interfaces/clients.entity.interface" + export interface FeedbackRatingCreateDTO { - client_id: string, + client_id: ClientsId, count?: number, rating_sum?: number } @@ -15,7 +17,7 @@ export interface FeedbackRatingResponseDTO { } export interface FeedbackRatingExtendedResponseDTO extends FeedbackRatingResponseDTO { - client_id: string, + client_id: ClientsId, count: number, rating_sum: number, last_modified: string, diff --git a/backend/src/dtos/feedback.dto.ts b/backend/src/dtos/feedback.dto.ts index 73e26eb..e042d8c 100644 --- a/backend/src/dtos/feedback.dto.ts +++ b/backend/src/dtos/feedback.dto.ts @@ -1,6 +1,10 @@ +import { ClientsId } from "../repositories/interfaces/clients.entity.interface" +import { FeedbackId } from "../repositories/interfaces/feedback.entity.interface" +import { UsersId } from "../repositories/interfaces/users.entity.interface" + export interface FeedbackCreateDTO { - client_id: string, - user_id: string, + client_id: ClientsId, + user_id: UsersId, rating: number, term_accepted: boolean, message?: string, @@ -19,8 +23,8 @@ export interface FeedbackUpdateReviewDTO { } export interface FeedbackFilterDTO { - client_id?: string | string[], - user_id?: string | string[], + client_id?: ClientsId | ClientsId[], + user_id?: UsersId | UsersId[], rating?: number | number[], term_accepted?: boolean, reviewed_on?: string | string[], @@ -29,9 +33,9 @@ export interface FeedbackFilterDTO { } export interface FeedbackResponseDTO { - feedback_id: number, - client_id: string, - user_id: string, + feedback_id: FeedbackId, + client_id: ClientsId, + user_id: UsersId, rating: number, rating_average_new?: number, rating_old?: number, @@ -39,5 +43,6 @@ export interface FeedbackResponseDTO { message?: string, reviewed_on?: string, last_modified: string, - created_on: string + created_on: string, + blocked?: boolean } diff --git a/backend/src/dtos/meta.dto.ts b/backend/src/dtos/meta.dto.ts index 4fc3b44..3216424 100644 --- a/backend/src/dtos/meta.dto.ts +++ b/backend/src/dtos/meta.dto.ts @@ -1,3 +1,4 @@ +import { MetaId } from "../repositories/interfaces/meta.entity.interface" import { EnvMode } from "../utils/enums/env-mode.enum" import { MaintenanceMode } from "../utils/enums/maintenance-mode.enum" @@ -15,7 +16,7 @@ export interface MetaUpdateDTO { } export interface MetaResponseDTO { - id: number, + id: MetaId, app: string, author: string, build_on: string, @@ -36,7 +37,7 @@ export interface MaintenanceUpdateDTO { } export interface MaintenanceResponseDTO { - id: number, + id: MetaId, app: string, build_on: string, maintenance_mode: MaintenanceMode, diff --git a/backend/src/dtos/rate-limits.dto.ts b/backend/src/dtos/rate-limits.dto.ts index bf6fb39..e6cc154 100644 --- a/backend/src/dtos/rate-limits.dto.ts +++ b/backend/src/dtos/rate-limits.dto.ts @@ -1,25 +1,29 @@ +import { ClientsId } from "../repositories/interfaces/clients.entity.interface" +import { RateLimitsId } from "../repositories/interfaces/rate-limits.entity.interface" +import { UsersId } from "../repositories/interfaces/users.entity.interface" + export interface RateLimitsCountDTO { - client_id?: string, - user_id?: string, + client_id?: ClientsId, + user_id?: UsersId, day: string } export interface RateLimitsCreateDTO { - client_id: string, - user_id: string + client_id: ClientsId, + user_id: UsersId } export interface RateLimitsUpdateDTO { - client_id: string, - user_id: string, + client_id: ClientsId, + user_id: UsersId, day?: string, last_modified?: string } export interface RateLimitsResponseDTO { - rate_limit_id: number, - client_id: string, - user_id: string, + rate_limit_id: RateLimitsId, + client_id: ClientsId, + user_id: UsersId, day: string, count: number, last_modified: string diff --git a/backend/src/dtos/tickets.dto.ts b/backend/src/dtos/tickets.dto.ts index 79fd8db..d894b47 100644 --- a/backend/src/dtos/tickets.dto.ts +++ b/backend/src/dtos/tickets.dto.ts @@ -1,18 +1,20 @@ -import { SingleOrArray } from "../utils/custom-types.utils"; +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; +import { TicketsId } from "../repositories/interfaces/tickets.entity.interface"; +import { UsersId } from "../repositories/interfaces/users.entity.interface"; import { DeviceOption } from "../utils/enums/device-option.enum"; import { Flag } from "../utils/enums/flag.enum"; import { TicketOption } from "../utils/enums/ticket-option.enum"; import { TicketStatus } from "../utils/enums/ticket-status.enum"; export interface TicketsIntervalDTO { - client_id?: string, - user_id?: string, + client_id?: ClientsId, + user_id?: UsersId, intervalTime: string } export interface TicketsCreateDTO { - client_id: string, - user_id: string, + client_id: ClientsId, + user_id: UsersId, option: TicketOption, title: string, message: string, @@ -49,20 +51,20 @@ export interface TicketsUpdateDTO { } export interface TicketsFilterDTO { - client_id?: SingleOrArray, - user_id?: SingleOrArray, + client_id?: ClientsId | ClientsId[], + user_id?: UsersId | UsersId[], title?: string, - status?: SingleOrArray, - option?: SingleOrArray, - flag?: SingleOrArray | null, + status?: TicketStatus | TicketStatus[], + option?: TicketOption | TicketOption[], + flag?: Flag | Flag[] | null, last_modified?: string[], created_on?: string[] } export interface TicketsResponseDTO { - ticket_id: string, - client_id: string, - user_id: string, + ticket_id: TicketsId, + client_id: ClientsId, + user_id: UsersId, status: TicketStatus, option: TicketOption, title: string, @@ -77,10 +79,10 @@ export interface TicketsResponseDTO { } export interface TicketsResponseExtendedDTO { - ticket_id: string, - client_id: string, + ticket_id: TicketsId, + client_id: ClientsId, client_name: string, - user_id: string, + user_id: UsersId, user_email: string, status: TicketStatus, option: TicketOption, diff --git a/backend/src/dtos/users.dto.ts b/backend/src/dtos/users.dto.ts index 0c14b92..67ff9c6 100644 --- a/backend/src/dtos/users.dto.ts +++ b/backend/src/dtos/users.dto.ts @@ -1,6 +1,6 @@ import { UserStatus } from "../utils/enums/user-status.enum"; import { Flag } from "../utils/enums/flag.enum"; -import { SingleOrArray } from "../utils/custom-types.utils"; +import { UsersId } from "../repositories/interfaces/users.entity.interface"; export interface UsersCreateDTO { email: string, @@ -19,15 +19,15 @@ export interface UsersFlagUpdateDTO { } export interface UsersFilterDTO { - email?: SingleOrArray, - status?: SingleOrArray, - flag?: SingleOrArray | null, + email?: string | string[], + status?: UserStatus | UserStatus[], + flag?: Flag | Flag[] | null, last_modified?: string[], created_on?: string[] } export interface UsersResponseDTO { - user_id: string, + user_id: UsersId, email: string, status: UserStatus, flag: Flag | null, diff --git a/backend/src/middleware/auth.admin.middleware.ts b/backend/src/middleware/auth.admin.middleware.ts index 3dd93b8..b20de12 100644 --- a/backend/src/middleware/auth.admin.middleware.ts +++ b/backend/src/middleware/auth.admin.middleware.ts @@ -17,7 +17,7 @@ export function authAdmin() { throw new MissingApiKeyException('support-missing-admin-auth'); } - const hasValidKey = adminKey.trim() === secrets.ADMIN_API.trim(); + const hasValidKey = adminKey.trim() === secrets.ADMIN_KEY.trim(); if(!hasValidKey) { throw new InvalidApiKeyException('support-invalid-admin-auth'); } diff --git a/backend/src/middleware/container/penalty.container.middleware.ts b/backend/src/middleware/container/penalty.container.middleware.ts index 036371d..b0ee529 100644 --- a/backend/src/middleware/container/penalty.container.middleware.ts +++ b/backend/src/middleware/container/penalty.container.middleware.ts @@ -11,6 +11,6 @@ export const penaltyHandler = new PenaltyHandler( new Map([ [Violation.CLIENTSFLAG, new ClientsFlagPenalty()], [Violation.USERSFLAG, new UsersFlagPenalty()], - [Violation.MAINTENANCE_TRAFFIC, new MaintenanceTrafficPenalty] + [Violation.MAINTENANCE_TRAFFIC, new MaintenanceTrafficPenalty()] ]) ) \ No newline at end of file diff --git a/backend/src/middleware/interfaces/penalties.interface.middleware.ts b/backend/src/middleware/interfaces/penalties.interface.middleware.ts index 76a1395..513b3f7 100644 --- a/backend/src/middleware/interfaces/penalties.interface.middleware.ts +++ b/backend/src/middleware/interfaces/penalties.interface.middleware.ts @@ -1,3 +1,6 @@ +import { ClientsId } from "../../repositories/interfaces/clients.entity.interface"; +import { MetaId } from "../../repositories/interfaces/meta.entity.interface"; +import { UsersId } from "../../repositories/interfaces/users.entity.interface"; import { Flag } from "../../utils/enums/flag.enum"; import { MaintenanceMode } from "../../utils/enums/maintenance-mode.enum"; import { Violation } from "../../utils/enums/violations.enum"; @@ -14,24 +17,24 @@ export interface PenaltyApply { } export type PenaltyContext = - | { type: Violation.CLIENTSFLAG, id: string, penaltyValue: Flag | null } - | { type: Violation.USERSFLAG, id: string, penaltyValue: Flag | null } - | { type: Violation.MAINTENANCE_TRAFFIC, id: number, penaltyValue: MaintenanceMode }; + | { type: Violation.CLIENTSFLAG, id: ClientsId, penaltyValue: Flag | null } + | { type: Violation.USERSFLAG, id: UsersId, penaltyValue: Flag | null } + | { type: Violation.MAINTENANCE_TRAFFIC, id: MetaId, penaltyValue: MaintenanceMode }; export interface PenaltyClientsFlagContext extends BasePenaltyContext { type: Violation.CLIENTSFLAG, - id: string, + id: ClientsId, penaltyValue: Flag | null } export interface PenaltyUsersFlagContext extends BasePenaltyContext { type: Violation.USERSFLAG, - id: string, + id: UsersId, penaltyValue: Flag | null } export interface PenaltyMaintenanceTrafficContext extends BasePenaltyContext { type: Violation.MAINTENANCE_TRAFFIC, - id: number, + id: MetaId, penaltyValue: MaintenanceMode } \ No newline at end of file diff --git a/backend/src/middleware/interfaces/rate-limits.interface.middleware.ts b/backend/src/middleware/interfaces/rate-limits.interface.middleware.ts index 704a202..11caa32 100644 --- a/backend/src/middleware/interfaces/rate-limits.interface.middleware.ts +++ b/backend/src/middleware/interfaces/rate-limits.interface.middleware.ts @@ -1,10 +1,10 @@ -import { Clients } from "../../repositories/interfaces/clients.entity.interface"; -import { Users } from "../../repositories/interfaces/users.entity.interface"; +import { Clients, ClientsId } from "../../repositories/interfaces/clients.entity.interface"; +import { Users, UsersId } from "../../repositories/interfaces/users.entity.interface"; import { PenaltyContext } from "./penalties.interface.middleware"; export interface RateLimitsData { - client_id: string, - user_id: string, + client_id: ClientsId, + user_id: UsersId, client?: Clients, user?: Users, } diff --git a/backend/src/middleware/observe.middleware.ts b/backend/src/middleware/observe.middleware.ts index b7e2ee1..6e056ba 100644 --- a/backend/src/middleware/observe.middleware.ts +++ b/backend/src/middleware/observe.middleware.ts @@ -14,6 +14,8 @@ import { RateLimitsData, RateLimitsResponse } from "./interfaces/rate-limits.int import { ExceedMaxEndpointException } from "../utils/exceptions/api.exception"; import { DemoLimitsIncrement, RateLimitsIncrement } from "./adapter/rate-limits.adapter.middleware"; import { penaltyHandler } from "./container/penalty.container.middleware"; +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../repositories/interfaces/users.entity.interface"; export function observe(isDemo: boolean = false) { return async function (req: Request, res: Response, next: NextFunction) { @@ -63,5 +65,5 @@ async function checkDemoLimits(): Promise { ], new DemoLimitsIncrement() ); - return await engine.process({ client_id: 'demo', user_id: 'demo'}); + return await engine.process({ client_id: 'demo' as ClientsId, user_id: 'demo' as UsersId}); } \ No newline at end of file diff --git a/backend/src/middleware/rules/rate-limits.rule.middleware.ts b/backend/src/middleware/rules/rate-limits.rule.middleware.ts index 15fac2a..2c09866 100644 --- a/backend/src/middleware/rules/rate-limits.rule.middleware.ts +++ b/backend/src/middleware/rules/rate-limits.rule.middleware.ts @@ -1,6 +1,7 @@ import { DemoLimitsCountDTO } from "../../dtos/demo-limits.dto"; import { RateLimitsCountDTO } from "../../dtos/rate-limits.dto"; import { TicketsIntervalDTO } from "../../dtos/tickets.dto"; +import { MetaId } from "../../repositories/interfaces/meta.entity.interface"; import demoLimitsService from "../../services/demo-limits.service"; import rateLimitsService from "../../services/rate-limits.service"; import ticketsService from "../../services/tickets.service"; @@ -108,7 +109,7 @@ export class TotalDailyLimitRule implements RateLimitsRule { retryAfter: CommonUtils.getNextDayUTC(), penalty: { type: Violation.MAINTENANCE_TRAFFIC, - id: 1, + id: 1 as MetaId, penaltyValue: MaintenanceMode.T011 } }; diff --git a/backend/src/models/clients.model.ts b/backend/src/models/clients.model.ts index ecf980a..4c74f10 100644 --- a/backend/src/models/clients.model.ts +++ b/backend/src/models/clients.model.ts @@ -2,7 +2,7 @@ import { ClientsCreateDTO, ClientsCreateResponseDTO, } from "../dtos/clients.dto"; -import { Clients } from "../repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../repositories/interfaces/clients.entity.interface"; import crypto from 'crypto'; import * as CommonUtils from "../utils/common.utils"; import { ApiKeyStatus } from "../utils/enums/api-key-status.enum"; @@ -14,7 +14,7 @@ class ClientsModel { this.timeMapTargets = ['last_use', 'last_modified', 'created_on']; } - mapToCreateResponseDTO(data: Clients, apiKey: string): ClientsCreateResponseDTO { + toClientsCreateResponseDTO(data: Clients, apiKey: string): ClientsCreateResponseDTO { data = CommonUtils.mapObjTimestamps(data, this.timeMapTargets); return { client_id: data.client_id, @@ -28,7 +28,7 @@ class ClientsModel { }; } - _generateApiKeyObj(): { keyRaw: string, keyHash: string } { + private generateApiKeyObj(): { keyRaw: string, keyHash: string } { const keyLength = 42; const charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const bytes = crypto.randomBytes(keyLength); @@ -43,8 +43,8 @@ class ClientsModel { } generateClientsCreateObj(dto: ClientsCreateDTO): { client: Clients, keyRaw: string } { - const id = CommonUtils.generateUUID(); - const keyObj = this._generateApiKeyObj(); + const id = CommonUtils.generateUUID(); + const keyObj = this.generateApiKeyObj(); const timestamp = CommonUtils.getTimestampUTC(); const client: Clients = { client_id: id, diff --git a/backend/src/models/test.model.ts b/backend/src/models/test.model.ts index 35fab18..d8dfe50 100644 --- a/backend/src/models/test.model.ts +++ b/backend/src/models/test.model.ts @@ -3,7 +3,7 @@ import { MaintenanceMode } from "../utils/enums/maintenance-mode.enum"; import { ExceedMaxEndpointException, UnexpectedApiResponseException, UnimplementedException } from "../utils/exceptions/api.exception"; import { AuthSecretNotFoundException, BlockedUsersException, ForbiddenApiKeyException, InvalidApiKeyException, InvalidUsersException, MalformedApiKeyException, MissingApiKeyException, PermissionException } from "../utils/exceptions/auth.exception"; import { InternalServerException, InvalidSourceException, MaintenanceException, RequestExceedMaxException } from "../utils/exceptions/common.exception"; -import { DBConnectionException, DBEmptyException, DBQueryErrorException } from "../utils/exceptions/db.exception"; +import { DBConnectionException, DBConstraintErrorException, DBEmptyException, DBQueryErrorException } from "../utils/exceptions/db.exception"; import { InvalidFilesException, InvalidPropertiesException } from "../utils/exceptions/validation.exception"; class TestModel { @@ -44,6 +44,8 @@ class TestModel { throw new DBEmptyException(); case('DBQueryErrorException'): throw new DBQueryErrorException(); + case('DBConstraintErrorException'): + throw new DBConstraintErrorException(dto.errorMsg ?? this.substitutionMsg); // ValidationException case('InvalidPropertiesException'): { const customError = [ diff --git a/backend/src/models/tickets.model.ts b/backend/src/models/tickets.model.ts index 2a908f9..2b011ee 100644 --- a/backend/src/models/tickets.model.ts +++ b/backend/src/models/tickets.model.ts @@ -1,14 +1,14 @@ import { TicketsCreateDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../dtos/tickets.dto"; -import { Tickets } from "../repositories/interfaces/tickets.entity.interface"; +import { Tickets, TicketsId } from "../repositories/interfaces/tickets.entity.interface"; import * as CommonUtils from "../utils/common.utils"; import { TicketStatus } from "../utils/enums/ticket-status.enum"; import { FilesService } from "../services/files.service"; import { PermissionException } from "../utils/exceptions/auth.exception"; class TicketsModel { - async generateTicket(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise { + async generateTicketEntity(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise { const timestamp = CommonUtils.getTimestampUTC(); - const newId = CommonUtils.generateUUID(); + const newId = CommonUtils.generateUUID(); let paths: string[] | null = null; if(files) { const filesService = new FilesService(files, 'tickets'); diff --git a/backend/src/models/users.model.ts b/backend/src/models/users.model.ts index ab34214..849b2d1 100644 --- a/backend/src/models/users.model.ts +++ b/backend/src/models/users.model.ts @@ -1,13 +1,13 @@ import { UsersCreateDTO } from "../dtos/users.dto"; -import { Users } from "../repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../repositories/interfaces/users.entity.interface"; import * as CommonUtils from "../utils/common.utils"; import { UserStatus } from "../utils/enums/user-status.enum"; class UsersModel { - generateUser(dto: UsersCreateDTO): Users { + generateUserEntity(dto: UsersCreateDTO): Users { const timestamp = CommonUtils.getTimestampUTC(); return { - user_id: CommonUtils.generateUUID(), + user_id: CommonUtils.generateUUID(), email: dto.email, status: UserStatus.ACTIVE, flag: null, diff --git a/backend/src/repositories/clients.repository.ts b/backend/src/repositories/clients.repository.ts index 26adc3e..6e85de4 100644 --- a/backend/src/repositories/clients.repository.ts +++ b/backend/src/repositories/clients.repository.ts @@ -1,4 +1,4 @@ -import { Clients } from "./interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "./interfaces/clients.entity.interface"; import { DBConnection } from "../configs/db"; import { QueryResult } from "pg"; import { ApiKeyStatus } from "../utils/enums/api-key-status.enum"; @@ -13,7 +13,7 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi this.table = "clients"; } - async findById(id: string): Promise { + async findById(id: ClientsId): Promise { const filterColumn = 'client_id'; const sql = `SELECT * FROM ${this.table} WHERE ${filterColumn} = $1;`; const value = [id]; @@ -94,7 +94,7 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi } } - async updateFlag(id: string, dto: Partial): Promise { + async updateFlag(id: ClientsId, dto: Partial): Promise { const filterColumn = "client_id"; const sql = `UPDATE ${this.table} SET flag = $1, last_modified = $2 @@ -118,7 +118,7 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi } } - async updateStatus(id: string, dto: Partial): Promise { + async updateStatus(id: ClientsId, dto: Partial): Promise { const filterColumn = "client_id"; const sql = `UPDATE ${this.table} SET status = $1, last_modified = $2 @@ -142,7 +142,7 @@ class ClientsRepository implements ICreateRepository, IUpdateFlagReposi } } - async updateLastUse(id: string, dto: Partial): Promise { + async updateLastUse(id: ClientsId, dto: Partial): Promise { const filterColumn = "client_id"; const sql = `UPDATE ${this.table} SET last_use = $1 diff --git a/backend/src/repositories/feedback-rating.repository.ts b/backend/src/repositories/feedback-rating.repository.ts index 84e9a82..d0251f6 100644 --- a/backend/src/repositories/feedback-rating.repository.ts +++ b/backend/src/repositories/feedback-rating.repository.ts @@ -5,6 +5,7 @@ import { DBQueryErrorException } from "../utils/exceptions/db.exception"; import { IFindRepository } from "./interfaces/base.repository.interface"; import { FeedbackRating } from "./interfaces/feedback-rating.entity.interface"; import { FeedbackRatingUpdateDTO } from "../dtos/feedback-rating.dto"; +import { ClientsId } from "./interfaces/clients.entity.interface"; class FeedbackRatingRepository implements IFindRepository { @@ -14,7 +15,7 @@ IFindRepository { this.table = 'feedback_ratings'; } - async findById(id: string): Promise { + async findById(id: ClientsId): Promise { const filterColumn = 'client_id'; const sql = `SELECT * FROM ${this.table} WHERE ${filterColumn} = $1;`; const value = [id]; @@ -92,7 +93,7 @@ IFindRepository { /** * @description Repository function to call only when used within a transaction => needs PoolClient as param. */ - async updateInTa(client: PoolClient, id: string, dto: FeedbackRatingUpdateDTO): Promise { + async updateInTa(client: PoolClient, id: ClientsId, dto: FeedbackRatingUpdateDTO): Promise { const filterColumn = 'client_id'; const sql = `UPDATE ${this.table} SET count = count + $1, rating_sum = rating_sum + $2, last_modified = $3::timestamp diff --git a/backend/src/repositories/feedback.repository.ts b/backend/src/repositories/feedback.repository.ts index 4b23fc6..d218d03 100644 --- a/backend/src/repositories/feedback.repository.ts +++ b/backend/src/repositories/feedback.repository.ts @@ -1,7 +1,7 @@ import { PoolClient, QueryResult } from "pg"; import { DBConnection } from "../configs/db"; import { FeedbackFilterDTO, FeedbackResponseDTO, FeedbackUpdateReviewDTO } from "../dtos/feedback.dto"; -import { Feedback } from "./interfaces/feedback.entity.interface"; +import { Feedback, FeedbackId } from "./interfaces/feedback.entity.interface"; import { DBQueryErrorException } from "../utils/exceptions/db.exception"; import { logError } from "../utils/common.utils"; import { mapFilteredQueryValues } from "../utils/repository.utils"; @@ -13,7 +13,7 @@ class FeedbackRepository { this.table = 'feedback_entries'; } - async findById(id: number): Promise { + async findById(id: FeedbackId): Promise { const filterColumn = "feedback_id"; const sql = `SELECT * FROM ${this.table} WHERE ${filterColumn} = $1;`; const value = [id]; @@ -51,7 +51,7 @@ class FeedbackRepository { } } - async updateReview(id: number, dto: FeedbackUpdateReviewDTO): Promise { + async updateReview(id: FeedbackId, dto: FeedbackUpdateReviewDTO): Promise { const filterColumn = "feedback_id"; const sql = `UPDATE ${this.table} SET reviewed_on = $1::timestamp, last_modified = $2::timestamp @@ -76,9 +76,21 @@ class FeedbackRepository { } /** + * Upsert feedback within a transaction. * - * @description Update on insert conflict for existing client_id and user_id combined entry (unique constraint). Is called within transaction only => needs PoolClient as param. - * @returns {FeedbackResponseDTO | null} Entity expanded by rating_old value for further processing. + * Uses a CTE-based pattern to handle the following cases: + * - UDPATE: Existing entry => updates and returns row + * - INSERT: Non existing entry (unique constraint for client_id & user_id combination) => creates new row + * - BLOCK: Existing entry with message but without review => update is prevented by WHERE NOT clause. + * Returns existing row with `blocked: true` to handle specific response. + * + * Notes: + * - UNION ALL: Combine results of multiple SELECT statements. + * - ON CONFLICT: PostgreSQL does NOT have UPSERT statement => supports INSERT...ON CONFLICT instead (or MERGE). + * + * @param {PoolClient} client Client used to connect database as this fn is called within a transaction. + * @param {Partial} entity All properties needed except 'feedback_id' due to serial type in database. + * @returns {FeedbackResponseDTO | null} FeedbackResponseDTO expands `Feedback` by 'rating_old' and 'blocked' value for further processing => `blocked: true` for prevented update or `NULL` if nothing was found (unexpected). */ async upsertInTa(client: PoolClient, entity: Partial): Promise { const sql = ` @@ -86,21 +98,30 @@ class FeedbackRepository { SELECT rating FROM ${this.table} WHERE client_id = $1 AND user_id = $2 + ), + upsert AS ( + INSERT INTO ${this.table} + (client_id, user_id, rating, term_accepted, message, reviewed_on, last_modified, created_on) + VALUES ($1, $2, $3, $4, $5, $6, $7::timestamp, $8::timestamp) + ON CONFLICT (client_id, user_id) + DO UPDATE SET + rating = EXCLUDED.rating, + term_accepted = EXCLUDED.term_accepted, + message = EXCLUDED.message, + reviewed_on = EXCLUDED.reviewed_on, + last_modified = EXCLUDED.last_modified + WHERE NOT (${this.table}.message IS NOT NULL AND ${this.table}.reviewed_on IS NULL) + RETURNING + ${this.table}.*, + (SELECT rating FROM pre_update_data) AS rating_old, + false AS blocked ) - INSERT INTO ${this.table} - (client_id, user_id, rating, term_accepted, message, reviewed_on, last_modified, created_on) - VALUES ($1, $2, $3, $4, $5, $6, $7::timestamp, $8::timestamp) - ON CONFLICT (client_id, user_id) - DO UPDATE SET - rating = EXCLUDED.rating, - term_accepted = EXCLUDED.term_accepted, - message = EXCLUDED.message, - reviewed_on = EXCLUDED.reviewed_on, - last_modified = EXCLUDED.last_modified - WHERE NOT (${this.table}.message IS NOT NULL AND ${this.table}.reviewed_on IS NOT NULL) - RETURNING - ${this.table}.*, - (SELECT rating FROM pre_update_data) AS rating_old; + SELECT * FROM upsert + UNION ALL + SELECT *, NULL AS rating_old, true AS blocked + FROM ${this.table} + WHERE client_id = $1 AND user_id = $2 + AND NOT EXISTS (SELECT 1 FROM upsert) `; const values = [entity.client_id, entity.user_id, entity.rating, entity.term_accepted, entity.message, null, entity.last_modified, entity.created_on]; const result: QueryResult = await client.query(sql, values); diff --git a/backend/src/repositories/interfaces/clients.entity.interface.ts b/backend/src/repositories/interfaces/clients.entity.interface.ts index c2d1cd3..67c3d76 100644 --- a/backend/src/repositories/interfaces/clients.entity.interface.ts +++ b/backend/src/repositories/interfaces/clients.entity.interface.ts @@ -1,8 +1,10 @@ import { ApiKeyStatus } from "../../utils/enums/api-key-status.enum"; import { Flag } from "../../utils/enums/flag.enum"; +export type ClientsId = string & { readonly brand: unique symbol }; + export interface Clients { - client_id: string, + client_id: ClientsId, name: string, api_key_hash: string, status: ApiKeyStatus, diff --git a/backend/src/repositories/interfaces/demo-limits.entity.interface.ts b/backend/src/repositories/interfaces/demo-limits.entity.interface.ts index 4569370..9d756d0 100644 --- a/backend/src/repositories/interfaces/demo-limits.entity.interface.ts +++ b/backend/src/repositories/interfaces/demo-limits.entity.interface.ts @@ -1,5 +1,7 @@ +export type DemoLimitsId = number & { readonly brand: unique symbol }; + export interface DemoLimits { - demo_limit_id: number, + demo_limit_id: DemoLimitsId, day: string, count: number, last_modified: string diff --git a/backend/src/repositories/interfaces/feedback-rating.entity.interface.ts b/backend/src/repositories/interfaces/feedback-rating.entity.interface.ts index 0cffb89..aeee659 100644 --- a/backend/src/repositories/interfaces/feedback-rating.entity.interface.ts +++ b/backend/src/repositories/interfaces/feedback-rating.entity.interface.ts @@ -1,6 +1,8 @@ +import { ClientsId } from "./clients.entity.interface"; + // Use Clients FK as PK => 1:1 relationship as client is unique for each feedback-rating. export interface FeedbackRating { - client_id: string, + client_id: ClientsId, count: number, rating_sum: number, last_modified: string, diff --git a/backend/src/repositories/interfaces/feedback.entity.interface.ts b/backend/src/repositories/interfaces/feedback.entity.interface.ts index 33fe571..79909c8 100644 --- a/backend/src/repositories/interfaces/feedback.entity.interface.ts +++ b/backend/src/repositories/interfaces/feedback.entity.interface.ts @@ -1,7 +1,12 @@ +import { ClientsId } from "./clients.entity.interface"; +import { UsersId } from "./users.entity.interface"; + +export type FeedbackId = number & { readonly brand: unique symbol }; + export interface Feedback { - feedback_id: number, - client_id: string, - user_id: string, + feedback_id: FeedbackId, + client_id: ClientsId, + user_id: UsersId, rating: number, term_accepted: boolean, message?: string, diff --git a/backend/src/repositories/interfaces/meta.entity.interface.ts b/backend/src/repositories/interfaces/meta.entity.interface.ts index 51c56d4..0a5b7bc 100644 --- a/backend/src/repositories/interfaces/meta.entity.interface.ts +++ b/backend/src/repositories/interfaces/meta.entity.interface.ts @@ -1,8 +1,10 @@ import { EnvMode } from "../../utils/enums/env-mode.enum"; import { MaintenanceMode } from "../../utils/enums/maintenance-mode.enum"; +export type MetaId = number & { readonly brand: unique symbol }; + export interface Meta { - id: number, + id: MetaId, app: string, author: string, build_on: string, @@ -18,7 +20,7 @@ export interface Meta { } export interface Maintenance { - id: number, + id: MetaId, app: string, build_on: string, maintenance_mode: MaintenanceMode, diff --git a/backend/src/repositories/interfaces/rate-limits.entity.interface.ts b/backend/src/repositories/interfaces/rate-limits.entity.interface.ts index 66d2221..2260ade 100644 --- a/backend/src/repositories/interfaces/rate-limits.entity.interface.ts +++ b/backend/src/repositories/interfaces/rate-limits.entity.interface.ts @@ -1,7 +1,12 @@ +import { ClientsId } from "./clients.entity.interface"; +import { UsersId } from "./users.entity.interface"; + +export type RateLimitsId = number & { readonly brand: unique symbol }; + export interface RateLimits { - rate_limit_id: number, - client_id: string, - user_id: string, + rate_limit_id: RateLimitsId, + client_id: ClientsId, + user_id: UsersId, day: string, count: number, last_modified: string diff --git a/backend/src/repositories/interfaces/tickets.entity.interface.ts b/backend/src/repositories/interfaces/tickets.entity.interface.ts index 20c31c9..4a482fe 100644 --- a/backend/src/repositories/interfaces/tickets.entity.interface.ts +++ b/backend/src/repositories/interfaces/tickets.entity.interface.ts @@ -2,11 +2,15 @@ import { DeviceOption } from "../../utils/enums/device-option.enum"; import { Flag } from "../../utils/enums/flag.enum"; import { TicketOption } from "../../utils/enums/ticket-option.enum"; import { TicketStatus } from "../../utils/enums/ticket-status.enum"; +import { ClientsId } from "./clients.entity.interface"; +import { UsersId } from "./users.entity.interface"; + +export type TicketsId = string & { readonly brand: unique symbol }; export interface Tickets { - ticket_id: string, - client_id: string, - user_id: string, + ticket_id: TicketsId, + client_id: ClientsId, + user_id: UsersId, status: TicketStatus, option: TicketOption, title: string, diff --git a/backend/src/repositories/interfaces/users.entity.interface.ts b/backend/src/repositories/interfaces/users.entity.interface.ts index c387478..80384f3 100644 --- a/backend/src/repositories/interfaces/users.entity.interface.ts +++ b/backend/src/repositories/interfaces/users.entity.interface.ts @@ -1,8 +1,10 @@ import { Flag } from "../../utils/enums/flag.enum"; import { UserStatus } from "../../utils/enums/user-status.enum"; +export type UsersId = string & { readonly brand: unique symbol }; + export interface Users { - user_id: string, + user_id: UsersId, email: string, status: UserStatus, flag: Flag | null, diff --git a/backend/src/repositories/meta.repository.ts b/backend/src/repositories/meta.repository.ts index 0689e63..1d99bad 100644 --- a/backend/src/repositories/meta.repository.ts +++ b/backend/src/repositories/meta.repository.ts @@ -1,7 +1,7 @@ import { QueryResult } from "pg"; import { DBConnection } from "../configs/db"; import { IBaseRepository, IFindRepository } from "./interfaces/base.repository.interface"; -import { Maintenance, Meta } from "./interfaces/meta.entity.interface"; +import { Maintenance, Meta, MetaId } from "./interfaces/meta.entity.interface"; import { logError } from "../utils/common.utils"; import { DBQueryErrorException } from "../utils/exceptions/db.exception"; @@ -13,7 +13,7 @@ class MetaRepository implements IBaseRepository, IFindRepository { this.table = "meta"; } - async findById(id: number): Promise { + async findById(id: MetaId): Promise { const filterColumn = "id"; const sql = `SELECT id, app, author, build_on, environment, app_version, db_version, docker_image, docker_version, jenkins_version, maintenance_mode, last_modified, created_on @@ -103,7 +103,7 @@ class MetaRepository implements IBaseRepository, IFindRepository { } } - async update(id: number, dto: Partial): Promise { + async update(id: MetaId, dto: Partial): Promise { const filterColumn = "id"; const sql = `UPDATE ${this.table} SET app = $1, author = $2, build_on = $3, environment = $4, app_version = $5, db_version = $6, @@ -129,7 +129,7 @@ class MetaRepository implements IBaseRepository, IFindRepository { } } - async updateMaintenance(id: number, dto: Partial): Promise { + async updateMaintenance(id: MetaId, dto: Partial): Promise { const filterColumn = "id"; const sql = `UPDATE ${this.table} SET maintenance_mode = $1, last_modified = $2 diff --git a/backend/src/repositories/tickets.repository.ts b/backend/src/repositories/tickets.repository.ts index 79046f3..d9a5a9d 100644 --- a/backend/src/repositories/tickets.repository.ts +++ b/backend/src/repositories/tickets.repository.ts @@ -6,7 +6,7 @@ import { } from "./interfaces/base.repository.interface"; import { DBConnection } from "../configs/db"; import { QueryResult } from "pg"; -import { Tickets } from "./interfaces/tickets.entity.interface"; +import { Tickets, TicketsId } from "./interfaces/tickets.entity.interface"; import { TicketsFilterDTO, TicketsIntervalDTO, TicketsResponseExtendedDTO } from "../dtos/tickets.dto"; import { DBQueryErrorException } from "../utils/exceptions/db.exception"; import { logError, now } from "../utils/common.utils"; @@ -24,7 +24,7 @@ IDeleteRepository this.table = "tickets"; } - async findById(id: string): Promise { + async findById(id: TicketsId): Promise { const filterColumn = "ticket_id"; const sql = `SELECT ${this.table}.*, @@ -138,7 +138,7 @@ IDeleteRepository } } - async update(id: string, dto: Partial): Promise { + async update(id: TicketsId, dto: Partial): Promise { const filterColumn = "ticket_id"; const sql = `UPDATE ${this.table} SET status = $1, title = $2, message = $3, option = $4, flag = $5, info_browser = $6, info_os = $7, @@ -163,7 +163,7 @@ IDeleteRepository } } - async delete(id: string): Promise { + async delete(id: TicketsId): Promise { const filterColumn = "ticket_id"; const sql = `DELETE FROM ${this.table} WHERE ${filterColumn} = $1;`; const value = [id]; diff --git a/backend/src/repositories/users.repository.ts b/backend/src/repositories/users.repository.ts index 063a3cd..d579c99 100644 --- a/backend/src/repositories/users.repository.ts +++ b/backend/src/repositories/users.repository.ts @@ -1,7 +1,7 @@ import { DBConnection } from "../configs/db"; import { QueryResult } from "pg"; import { IBaseRepository, ICreateRepository, IFindRepository, IUpdateFlagRepository } from "./interfaces/base.repository.interface"; -import { Users } from "./interfaces/users.entity.interface"; +import { Users, UsersId } from "./interfaces/users.entity.interface"; import { logError } from "../utils/common.utils"; import { UsersFilterDTO } from "../dtos/users.dto"; import { DBQueryErrorException } from "../utils/exceptions/db.exception"; @@ -19,7 +19,7 @@ IUpdateFlagRepository this.table = "users"; } - async findById(id: string): Promise { + async findById(id: UsersId): Promise { const filterColumn = "user_id"; const sql = `SELECT * FROM ${this.table} WHERE ${filterColumn} = $1;`; const value = [id]; @@ -122,7 +122,7 @@ IUpdateFlagRepository } } - async update(id: string, dto: Partial): Promise { + async update(id: UsersId, dto: Partial): Promise { const filterColumn = "user_id"; const sql = `UPDATE ${this.table} SET email = $1, status = $2, flag = $3, last_modified = $4 @@ -146,7 +146,7 @@ IUpdateFlagRepository } } - async updateFlag(id: string, dto: Partial): Promise { + async updateFlag(id: UsersId, dto: Partial): Promise { const filterColumn = "user_id"; const sql = `UPDATE ${this.table} SET flag = $1, last_modified = $2 diff --git a/backend/src/routes/meta.route.ts b/backend/src/routes/meta.route.ts index 79ef5c7..26777e9 100644 --- a/backend/src/routes/meta.route.ts +++ b/backend/src/routes/meta.route.ts @@ -15,7 +15,7 @@ const router = Router(); // findById router.get( - '/by-id/:id', + '/id/:id', authAdmin(), byIdSchema, factory(metaController.getMetaById) @@ -23,7 +23,7 @@ router.get( // findByName router.get( - '/by-name/:name', + '/name/:name', authAdmin(), byNameSchema, factory(metaController.getMetaByName) diff --git a/backend/src/routes/tickets.route.ts b/backend/src/routes/tickets.route.ts index 3866b0f..f56083d 100644 --- a/backend/src/routes/tickets.route.ts +++ b/backend/src/routes/tickets.route.ts @@ -22,7 +22,7 @@ const router = Router(); // findById router.get( - '/by-id/:id', + '/id/:id', authAdmin(), getSchema, factory(ticketsController.getTicket) diff --git a/backend/src/routes/users.route.ts b/backend/src/routes/users.route.ts index 181f349..67ec5cd 100644 --- a/backend/src/routes/users.route.ts +++ b/backend/src/routes/users.route.ts @@ -16,7 +16,7 @@ const router = Router(); // findById router.get( - '/by-id/:id', + '/id/:id', authAdmin(), byIdSchema, factory(usersController.getUserById) @@ -24,7 +24,7 @@ router.get( // findByEmail router.get( - '/by-email/:email', + '/email/:email', authAdmin(), byEmailSchema, factory(usersController.getUserByEmail) diff --git a/backend/src/services/clients.service.ts b/backend/src/services/clients.service.ts index 4876f8c..b0160ba 100644 --- a/backend/src/services/clients.service.ts +++ b/backend/src/services/clients.service.ts @@ -12,7 +12,7 @@ import { import clientsModel from '../models/clients.model'; import * as CommonUtils from '../utils/common.utils'; import clientsRepository from '../repositories/clients.repository'; -import { Clients } from "../repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../repositories/interfaces/clients.entity.interface"; class ClientsService { private timeMapTargets: string[]; @@ -24,7 +24,7 @@ class ClientsService { /** * @description Usage for testing purpose. */ - async getClientById(id: string): Promise { + async getClientById(id: ClientsId): Promise { const result = await clientsRepository.findById(id); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } @@ -46,22 +46,22 @@ class ClientsService { async createClient(dto: ClientsCreateDTO): Promise { const clientsCreateObj = clientsModel.generateClientsCreateObj(dto); const result = await clientsRepository.create(clientsCreateObj.client); - return clientsModel.mapToCreateResponseDTO(result as Clients, clientsCreateObj.keyRaw); + return clientsModel.toClientsCreateResponseDTO(result as Clients, clientsCreateObj.keyRaw); } - async updateClientFlag(id: string, dto: ClientsFlagUpdateDTO): Promise { + async updateClientFlag(id: ClientsId, dto: ClientsFlagUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await clientsRepository.updateFlag(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async updateClientStatus(id: string, dto: ClientsStatusUpdateDTO): Promise { + async updateClientStatus(id: ClientsId, dto: ClientsStatusUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await clientsRepository.updateStatus(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async updateClientLastUse(id: string): Promise { + async updateClientLastUse(id: ClientsId): Promise { const dto: ClientsLastUseUpdateDTO = { last_use: CommonUtils.getTimestampUTC() }; const result = await clientsRepository.updateLastUse(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); diff --git a/backend/src/services/feedback-rating.service.ts b/backend/src/services/feedback-rating.service.ts index 262a87d..b733ade 100644 --- a/backend/src/services/feedback-rating.service.ts +++ b/backend/src/services/feedback-rating.service.ts @@ -8,15 +8,10 @@ import { PoolClient } from 'pg'; import feedbackRatingModel from "../models/feedback-rating.model"; import feedbackRatingRepository from "../repositories/feedback-rating.repository"; import { FeedbackRating } from "../repositories/interfaces/feedback-rating.entity.interface"; +import { ClientsId } from "../repositories/interfaces/clients.entity.interface"; class FeedbackRatingService { - private timeMapTargets: string[]; - - constructor() { - this.timeMapTargets = ['last_modified', 'created_on']; - } - - async getExtendedFeedbackRatingById(id: string): Promise { + async getExtendedFeedbackRatingById(id: ClientsId): Promise { const result: FeedbackRating | null = await feedbackRatingRepository.findById(id); if(!result) { return null; @@ -55,7 +50,7 @@ class FeedbackRatingService { /** * @description Service function to call only when used within a transaction => needs PoolClient as param. */ - async updateFeedbackRatingInTa(client: PoolClient, id: string, dto: FeedbackRatingUpdateDTO): Promise { + async updateFeedbackRatingInTa(client: PoolClient, id: ClientsId, dto: FeedbackRatingUpdateDTO): Promise { dto = feedbackRatingModel.mapFeedbackRatingUpdateDTO(dto); const result: FeedbackRating | null = await feedbackRatingRepository.updateInTa(client, id, dto); if(!result) { diff --git a/backend/src/services/feedback.service.ts b/backend/src/services/feedback.service.ts index aca3a74..87c1f40 100644 --- a/backend/src/services/feedback.service.ts +++ b/backend/src/services/feedback.service.ts @@ -9,12 +9,12 @@ import * as RepoUtils from "../utils/repository.utils"; import feedbackRatingModel from "../models/feedback-rating.model"; import feedbackModel from "../models/feedback.model"; import feedbackRepository from "../repositories/feedback.repository"; -import { Feedback } from "../repositories/interfaces/feedback.entity.interface"; +import { Feedback, FeedbackId } from "../repositories/interfaces/feedback.entity.interface"; import feedbackRatingService from "./feedback-rating.service"; +import { DBConstraintErrorException } from "../utils/exceptions/db.exception"; class FeedbackService { - - async getFeedbackById(id: number): Promise { + async getFeedbackById(id: FeedbackId): Promise { const result = await feedbackRepository.findById(id); return !result ? null : feedbackModel.toFeedbackResponseDTO(result); } @@ -37,6 +37,9 @@ class FeedbackService { let dtoUpdateFR: FeedbackRatingUpdateDTO; if(!result) { return null; + } else if(result.blocked) { + // Update on feedback is not allowed if message exists for entry without being reviewed yet. + throw new DBConstraintErrorException('support-constraint-feedback'); } else if(new Date(result.created_on).getTime() === new Date(entity.created_on!).getTime()) { // New Feedback was created => increase rating_sum. dtoUpdateFR = { count: 1, rating: dto.rating }; @@ -56,7 +59,8 @@ class FeedbackService { }; await feedbackRatingService.createFeedbackRatingInTa(client, dtoCreateFR); } - // Use rating from dto if no other ratings for this client exist. + // Delete blocked info and use rating from dto if no other ratings for this client exist. + delete result['blocked']; return { ...result, rating_average_new: update?.rating_average ?? dto.rating @@ -64,7 +68,7 @@ class FeedbackService { }) } - async updateFeedbackReview(id: number): Promise { + async updateFeedbackReview(id: FeedbackId): Promise { const dto: FeedbackUpdateReviewDTO = feedbackModel.generateFeedbackUpdateReviewDTO(); const result: Feedback | null = await feedbackRepository.updateReview(id, dto); return !result ? null : feedbackModel.toFeedbackResponseDTO(result); diff --git a/backend/src/services/meta.service.ts b/backend/src/services/meta.service.ts index 1fa3f47..9020b6a 100644 --- a/backend/src/services/meta.service.ts +++ b/backend/src/services/meta.service.ts @@ -4,6 +4,7 @@ import { MetaUpdateDTO, MaintenanceUpdateDTO, } from '../dtos/meta.dto'; +import { MetaId } from '../repositories/interfaces/meta.entity.interface'; import metaRepository from '../repositories/meta.repository'; import * as CommonUtils from "../utils/common.utils"; @@ -14,7 +15,7 @@ class MetaService { this.timeMapTargets = ['build_on', 'created_on', 'last_modified']; } - async getMetaById(id: number): Promise { + async getMetaById(id: MetaId): Promise { const result = await metaRepository.findById(id); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } @@ -29,7 +30,7 @@ class MetaService { return !result ? null : CommonUtils.mapArrayTimestamps(result, this.timeMapTargets); } - async updateMeta(id: number, dto: MetaUpdateDTO): Promise { + async updateMeta(id: MetaId, dto: MetaUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await metaRepository.update(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); @@ -40,7 +41,7 @@ class MetaService { return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async updateMaintenanceMode(id: number, dto: MaintenanceUpdateDTO): Promise { + async updateMaintenanceMode(id: MetaId, dto: MaintenanceUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await metaRepository.updateMaintenance(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); diff --git a/backend/src/services/test.service.ts b/backend/src/services/test.service.ts index bc86aac..fd4c4df 100644 --- a/backend/src/services/test.service.ts +++ b/backend/src/services/test.service.ts @@ -1,8 +1,8 @@ -import { MetaResponseDTO } from '../dtos/meta.dto'; import { TestDemoDTO, TestErrorDTO } from '../dtos/test.dto'; import testModel from '../models/test.model'; import metaRepository from '../repositories/meta.repository'; import { DemoMode } from '../utils/enums/demo-mode.enum'; +import { secrets } from '../utils/secrets.utils'; class TestService { async searchExceptionThrow(dto: TestErrorDTO) { @@ -13,8 +13,7 @@ class TestService { let result: any; switch(dto.demo_mode) { case(DemoMode.SUCCESS): { - result = await metaRepository.findById(1); - result = { app_version: (result as MetaResponseDTO).app_version }; + result = { app_version: secrets.APP_VERSION }; break; } case(DemoMode.ERROR): diff --git a/backend/src/services/tickets.service.ts b/backend/src/services/tickets.service.ts index f63e56f..2a2eacf 100644 --- a/backend/src/services/tickets.service.ts +++ b/backend/src/services/tickets.service.ts @@ -7,6 +7,7 @@ import { TicketsIntervalDTO } from "../dtos/tickets.dto"; import ticketsModel from "../models/tickets.model"; +import { TicketsId } from "../repositories/interfaces/tickets.entity.interface"; import ticketsRepository from "../repositories/tickets.repository"; import * as CommonUtils from "../utils/common.utils"; @@ -17,7 +18,7 @@ class TicketsService { this.timeMapTargets = ['last_modified', 'created_on']; } - async getTicketById(id: string): Promise { + async getTicketById(id: TicketsId): Promise { const result = await ticketsRepository.findById(id); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } @@ -38,18 +39,18 @@ class TicketsService { } async createTicket(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise { - const ticket = await ticketsModel.generateTicket(dto, files); + const ticket = await ticketsModel.generateTicketEntity(dto, files); const result = await ticketsRepository.create(ticket); return CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async updateTicket(id: string, dto: TicketsUpdateDTO): Promise { + async updateTicket(id: TicketsId, dto: TicketsUpdateDTO): Promise { dto = ticketsModel.mapTicketUpdateDto(dto); const result = await ticketsRepository.update(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async deleteTicket(id: string): Promise { + async deleteTicket(id: TicketsId): Promise { const ticket = await ticketsRepository.findById(id); if(ticket && ticketsModel.isPermittedToDelete(ticket)) { await ticketsModel.handleTicketBeforeDelete(ticket); diff --git a/backend/src/services/users.service.ts b/backend/src/services/users.service.ts index 372d6e2..d740749 100644 --- a/backend/src/services/users.service.ts +++ b/backend/src/services/users.service.ts @@ -8,7 +8,7 @@ import { import usersRepository from "../repositories/users.repository"; import * as CommonUtils from "../utils/common.utils"; import usersModel from "../models/users.model"; -import { Users } from "../repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../repositories/interfaces/users.entity.interface"; class UsersService { private timeMapTargets: string[]; @@ -17,7 +17,7 @@ class UsersService { this.timeMapTargets = ['last_modified', 'created_on']; } - async getUserById(id: string): Promise { + async getUserById(id: UsersId): Promise { const result = await usersRepository.findById(id); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } @@ -38,18 +38,18 @@ class UsersService { } async createUser(dto: UsersCreateDTO): Promise { - const user: Users = usersModel.generateUser(dto); + const user: Users = usersModel.generateUserEntity(dto); const result = await usersRepository.create(user); return CommonUtils.mapObjTimestamps(result, this.timeMapTargets) } - async updateUser(id: string, dto: UsersUpdateDTO): Promise { + async updateUser(id: UsersId, dto: UsersUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await usersRepository.update(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); } - async updateUserFlag(id: string, dto: UsersFlagUpdateDTO): Promise { + async updateUserFlag(id: UsersId, dto: UsersFlagUpdateDTO): Promise { dto.last_modified = CommonUtils.getTimestampUTC(); const result = await usersRepository.updateFlag(id, dto); return !result ? null : CommonUtils.mapObjTimestamps(result, this.timeMapTargets); diff --git a/backend/src/utils/common.utils.ts b/backend/src/utils/common.utils.ts index d625aec..49aefc1 100644 --- a/backend/src/utils/common.utils.ts +++ b/backend/src/utils/common.utils.ts @@ -13,8 +13,8 @@ const logger = Logger.getLogger(); */ export const now = (): Date => new Date(); -export function generateUUID(): string { - return uuid_v4(); +export function generateUUID(): T { + return uuid_v4() as T; } export function mapKeyToHash(key: string): string { @@ -40,19 +40,6 @@ export function isEmptyObj(obj: any): boolean { return JSON.stringify(obj) === '{}'; } -export function selectPrivateKey(source: MailSource): string { - switch(source) { - case(MailSource.ARTDV): { - return secrets.PRIVATE_KEY_ARTDV; - } - case(MailSource.TAVA): { - return secrets.PRIVATE_KEY_TAVA; - } - default: - throw new InvalidSourceException(); - } -} - export function logError(message: string, method: string, err: any) { logger.error(message, { error: err.error, diff --git a/backend/src/utils/custom-types.utils.ts b/backend/src/utils/custom-types.utils.ts index 50368d7..a14bdec 100644 --- a/backend/src/utils/custom-types.utils.ts +++ b/backend/src/utils/custom-types.utils.ts @@ -1,5 +1,3 @@ import { Request, Response, NextFunction } from "express"; -export type AsyncMiddleware = (req: Request, res: Response, next: NextFunction) => Promise; - -export type SingleOrArray = T | T[]; \ No newline at end of file +export type AsyncMiddleware = (req: Request, res: Response, next: NextFunction) => Promise; \ No newline at end of file diff --git a/backend/src/utils/exceptions/db.exception.ts b/backend/src/utils/exceptions/db.exception.ts index 4a93120..476eed6 100644 --- a/backend/src/utils/exceptions/db.exception.ts +++ b/backend/src/utils/exceptions/db.exception.ts @@ -38,4 +38,10 @@ export class DBQueryErrorException extends DBException { constructor(data?: any) { super('support-dbquery-error', data); } +} + +export class DBConstraintErrorException extends DBException { + constructor(message: string, data?: any) { + super(message, data); + } } \ No newline at end of file diff --git a/backend/src/utils/repository.utils.ts b/backend/src/utils/repository.utils.ts index 0ca5af9..cf6f15d 100644 --- a/backend/src/utils/repository.utils.ts +++ b/backend/src/utils/repository.utils.ts @@ -26,7 +26,7 @@ export function mapFilteredQueryValues>(dto: T, ta const argGroups: string[] = []; const timestampObj = {}; Object.entries(dto).forEach(([key, content]) => { - if(key !== 'reviewed_on' && key !== 'last_modified' && key !== 'created_on') { + if(key !== 'last_modified' && key !== 'created_on') { const valArr = Array.isArray(content) ? content : [content]; const conditions = valArr.map((value) => { if(value === null) { @@ -81,7 +81,11 @@ export async function asTransaction( } catch(err: any) { await client.query('ROLLBACK'); logError(message, method, err); - throw new DBQueryErrorException(err); + if(err.error) { + throw err; + } else { + throw new DBQueryErrorException(err); + } } finally { await db.close(client); } diff --git a/backend/src/utils/secrets.utils.ts b/backend/src/utils/secrets.utils.ts index ba06cef..06f27ae 100644 --- a/backend/src/utils/secrets.utils.ts +++ b/backend/src/utils/secrets.utils.ts @@ -6,14 +6,12 @@ import fs from "fs"; class Secrets { readonly APP_VERSION: string; readonly APP_META: Record; - readonly ADMIN_API: string; + readonly ADMIN_KEY: string; readonly ENV_MODE: string; readonly PORT: number; readonly EMAIL_RECEIVER: string; readonly EMAIL_SENDER: string; readonly EMAIL_PASS: string; - readonly PRIVATE_KEY_ARTDV: string; - readonly PRIVATE_KEY_TAVA: string; readonly BETTERSTACK_LOGGING_KEY: string; readonly BETTERSTACK_HOST: string; readonly DB_USER: string; @@ -44,14 +42,12 @@ class Secrets { private constructor() { this.APP_VERSION = this.getAppVersion(); this.APP_META = this.getAppMeta(); - this.ADMIN_API = this.setAdminApi(); + this.ADMIN_KEY = this.setAdminKey(); this.ENV_MODE = this.setEnvMode(); this.PORT = this.setPort(); this.EMAIL_RECEIVER = this.setEmailReceiver(); this.EMAIL_SENDER = this.setEmailSender(); this.EMAIL_PASS = this.setEmailPass(); - this.PRIVATE_KEY_ARTDV = this.setPrivateKey_ARTDV(); - this.PRIVATE_KEY_TAVA = this.setPrivateKey_TAVA(); this.BETTERSTACK_LOGGING_KEY = this.setBetterStackLoggingKey(); this.BETTERSTACK_HOST = this.setBetterStackHost(); this.DB_USER = this.setDbUser(); @@ -97,11 +93,11 @@ class Secrets { return !packageJson ? null : packageJson.appMeta; } - private setAdminApi() { - if(!Config.ADMIN_API) { - throw new AuthSecretNotFoundException('secret-404-env#ADMIN_API'); + private setAdminKey() { + if(!Config.ADMIN_KEY) { + throw new AuthSecretNotFoundException('secret-404-env#ADMIN_KEY'); } - return Config.ADMIN_API; + return Config.ADMIN_KEY; } private setEnvMode() { @@ -139,34 +135,6 @@ class Secrets { return Config.EMAIL_PASS; } - private setPrivateKey_ARTDV = () => { - let key; - if(Config.MODE === 'development') { - key = !Config.PRIVATE_KEY_ARTDV ? null : fs.readFileSync(Config.PRIVATE_KEY_ARTDV, 'utf8'); - } else { - key = Config.PRIVATE_KEY_ARTDV; - } - - if(!key) { - throw new AuthSecretNotFoundException('secret-404-env#PRIVATE_KEY_ARTDV'); - } - return key; - } - - private setPrivateKey_TAVA = () => { - let key; - if(Config.MODE === 'development') { - key = !Config.PRIVATE_KEY_TAVA ? null : fs.readFileSync(Config.PRIVATE_KEY_TAVA, 'utf8'); - } else { - key = Config.PRIVATE_KEY_TAVA; - } - - if(!key) { - throw new AuthSecretNotFoundException('secret-404-env#PRIVATE_KEY_TAVA'); - } - return key; - } - private setBetterStackLoggingKey = () => { if(!Config.BETTERSTACK_LOGGING_KEY) { throw new AuthSecretNotFoundException('secret-404-env#BETTERSTACK_LOGGING_KEY'); diff --git a/backend/src/validation/schemata/tickets.schema.validation.ts b/backend/src/validation/schemata/tickets.schema.validation.ts index 24aab0f..424ac08 100644 --- a/backend/src/validation/schemata/tickets.schema.validation.ts +++ b/backend/src/validation/schemata/tickets.schema.validation.ts @@ -2,7 +2,6 @@ import { body, param, ValidationChain } from 'express-validator'; import * as CommonValidators from "../common.validation"; import { Flag } from '../../utils/enums/flag.enum'; import { TicketStatus } from '../../utils/enums/ticket-status.enum'; -import { SingleOrArray } from '../../utils/custom-types.utils'; import { CommonExceptionMessage as Message } from '../../utils/enums/common-exception-messages.enum'; import { TicketOption } from '../../utils/enums/ticket-option.enum'; @@ -36,21 +35,21 @@ export const postTicketsSearchSchema: ValidationChain[] = [ .withMessage('support-invalid-max#title!100') .optional(), body('status') - .custom((content: SingleOrArray) => { + .custom((content: TicketStatus | TicketStatus[]) => { content = Array.isArray(content) ? content : [content]; content.forEach((status) => CommonValidators.validateEnum(status, TicketStatus, 'ticketStatus')) return true; }) .optional(), body('option') - .custom((content: SingleOrArray) => { + .custom((content: TicketOption | TicketOption[]) => { content = Array.isArray(content) ? content : [content]; content.forEach((option) => CommonValidators.validateEnum(option, TicketOption, 'ticketOption')) return true; }) .optional(), body('flag') - .custom((content: undefined | null | SingleOrArray) => { + .custom((content: undefined | null | Flag | Flag[]) => { // Manual check for undefined/null necessary because null is valid value. if(content === null || content === undefined) { return true; diff --git a/backend/src/validation/schemata/users.schema.validation.ts b/backend/src/validation/schemata/users.schema.validation.ts index aead4b3..3a08eb1 100644 --- a/backend/src/validation/schemata/users.schema.validation.ts +++ b/backend/src/validation/schemata/users.schema.validation.ts @@ -2,7 +2,6 @@ import { body, param, ValidationChain } from 'express-validator'; import * as CommonValidators from "../common.validation"; import { UserStatus } from '../../utils/enums/user-status.enum'; import { Flag } from '../../utils/enums/flag.enum'; -import { SingleOrArray } from '../../utils/custom-types.utils'; import { CommonExceptionMessage as Message } from '../../utils/enums/common-exception-messages.enum'; export const getUserByIdSchema: ValidationChain[] = [ @@ -22,21 +21,21 @@ export const getUserByEmailSchema: ValidationChain[] = [ export const postUsersSearchSchema: ValidationChain[] = [ body('email') - .custom((content: SingleOrArray) => { + .custom((content: string | string[]) => { content = Array.isArray(content) ? content : [content]; content.forEach((email) => CommonValidators.validateEmail(email)) return true; }) .optional(), body('status') - .custom((content: SingleOrArray) => { + .custom((content: UserStatus | UserStatus[]) => { content = Array.isArray(content) ? content : [content]; content.forEach((status) => CommonValidators.validateEnum(status, UserStatus, 'userStatus')) return true; }) .optional(), body('flag') - .custom((content: undefined | null | SingleOrArray) => { + .custom((content: undefined | null | Flag | Flag[]) => { // Manual check for undefined/null necessary because null is valid value. if(content === null || content === undefined) { return true; diff --git a/backend/tests/common.test-utils.ts b/backend/tests/common.test-utils.ts index d50c945..b1cb1ab 100644 --- a/backend/tests/common.test-utils.ts +++ b/backend/tests/common.test-utils.ts @@ -1,6 +1,8 @@ import { QueryResult } from "pg"; import { DBConnection } from "../src/configs/db"; import { NextFunction, Request, Response } from "express"; +import { UsersId } from "../src/repositories/interfaces/users.entity.interface"; +import { ClientsId } from "../src/repositories/interfaces/clients.entity.interface"; type MockClient = { query: jest.Mock @@ -40,7 +42,7 @@ export function disableConsoleMessages() { /** * @description Used in combination with createTestApp() to mock client authentication for flexible testing. */ -export function injectTestClientId(clientId: string) { +export function injectTestClientId(clientId: ClientsId) { return function (req: Request, res: Response, next: NextFunction) { (req as any).apiClients = { client_id: clientId }; next(); @@ -50,7 +52,7 @@ export function injectTestClientId(clientId: string) { /** * @description Used in combination with createTestApp() to mock user authentication for flexible testing. */ -export function injectTestUserId(userId: string) { +export function injectTestUserId(userId: UsersId) { return function (req: Request, res: Response, next: NextFunction) { (req as any).apiUsers = { user_id: userId }; next(); diff --git a/backend/tests/integration-tests/middleware/penalty.integration.test.ts b/backend/tests/integration-tests/middleware/penalty.integration.test.ts index 7b9fc1b..16033ec 100644 --- a/backend/tests/integration-tests/middleware/penalty.integration.test.ts +++ b/backend/tests/integration-tests/middleware/penalty.integration.test.ts @@ -10,12 +10,15 @@ import { runMigrations } from "../../db-migrations.setup"; import usersService from "../../../src/services/users.service"; import { MaintenanceMode } from "../../../src/utils/enums/maintenance-mode.enum"; import metaService from "../../../src/services/meta.service"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { MetaId } from "../../../src/repositories/interfaces/meta.entity.interface"; jest.setTimeout(60000); -const testValidMetaId = mockId.meta.valid[0]; -const testValidClientsId = mockId.clients.valid[0]; -const testValidUsersId = mockId.users.valid[0]; +const testValidMetaId = mockId.meta.valid[0] as MetaId; +const testValidClientId = mockId.clients.valid[0] as ClientsId; +const testValidUserId = mockId.users.valid[0] as UsersId; describe('Integration-tests (middleware), priority: class PenaltyHandler', () => { @@ -40,13 +43,13 @@ describe('Integration-tests (middleware), priority: class PenaltyHandler', () => test('Params: has Violation.CLIENTSFLAG, result: "PENALTY"', async () => { const testParam_context: PenaltyContext = { type: Violation.CLIENTSFLAG, - id: testValidClientsId, + id: testValidClientId, penaltyValue: null }; await dbTestSetup.addTestData(); const _ = await penaltyHandler.apply(testParam_context); - const testFn = await clientsService.getClientById(testValidClientsId); + const testFn = await clientsService.getClientById(testValidClientId); const testClientsFlagResult = Flag.WARNING; expect(testFn?.flag).toBe(testClientsFlagResult); @@ -55,13 +58,13 @@ describe('Integration-tests (middleware), priority: class PenaltyHandler', () => test('Params: has Violation.USERSFLAG, result: "PENALTY"', async () => { const testParam_context: PenaltyContext = { type: Violation.USERSFLAG, - id: testValidUsersId, + id: testValidUserId, penaltyValue: null }; await dbTestSetup.addTestData(); const _ = await penaltyHandler.apply(testParam_context); - const testFn = await usersService.getUserById(testValidUsersId); + const testFn = await usersService.getUserById(testValidUserId); const testUsersFlagResult = Flag.WARNING; expect(testFn?.flag).toBe(testUsersFlagResult); diff --git a/backend/tests/integration-tests/repositories/clients.integration.test.ts b/backend/tests/integration-tests/repositories/clients.integration.test.ts index a868924..e3831bc 100644 --- a/backend/tests/integration-tests/repositories/clients.integration.test.ts +++ b/backend/tests/integration-tests/repositories/clients.integration.test.ts @@ -16,7 +16,7 @@ import { ApiKeyStatus } from "../../../src/utils/enums/api-key-status.enum"; import { DBTestSetup } from "../../db-container.setup"; import { runMigrations } from '../../db-migrations.setup'; import clientsModel from "../../../src/models/clients.model"; -import { Clients } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import { CommonExceptionMessage } from "../../../src/utils/enums/common-exception-messages.enum"; import { default as mockId } from "../../mock-data/id.mock-data.json"; import { secrets } from "../../../src/utils/secrets.utils"; @@ -33,6 +33,8 @@ jest.mock('../../../src/middleware/maintenance.middleware', () => ({ import app from '../../../src/app'; jest.setTimeout(60000); + +const mockValidClientId = mockId.clients.valid[0] as ClientsId; const testTimestamp = '2025-01-01T14:00:02.000Z'; describe('Integration-tests (repository), priority: entity Clients', () => { @@ -59,9 +61,9 @@ describe('Integration-tests (repository), priority: entity Clients', () => { describe('Testing valid fn calls', () => { test('Repository process fn findById(), result: "SUCCESS"', async () => { - const testParam_id = mockId.clients.valid[0]; + const testParam_id = mockValidClientId; const testResult: ClientsExistResponseDTO | null = { - client_id: mockId.clients.valid[0], + client_id: testParam_id, name: 'TESTCLIENT', api_key_hash: secrets.TEST_APIKEY_HASH, status: ApiKeyStatus.ACTIVE, @@ -80,7 +82,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { test('Repository process fn findStatusByName(), result: "SUCCESS"', async () => { const testParam_name = 'TESTCLIENT'; const testResult: ClientsStatusResponseDTO = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, name: testParam_name, status: ApiKeyStatus.ACTIVE, last_use: testTimestamp, @@ -109,11 +111,11 @@ describe('Integration-tests (repository), priority: entity Clients', () => { }) test('Repository process fn create(), result: "SUCCESS"', async () => { - const testParam_client_id = mockId.clients.new[0]; + const testParam_client_id = mockId.clients.new[0] as ClientsId; const testParam_dto = { name: 'testclient_test_create' }; jest.spyOn(CommonUtils, 'generateUUID').mockReturnValue(testParam_client_id); - jest.spyOn(clientsModel, '_generateApiKeyObj').mockReturnValue(testVar_apiKey); + jest.spyOn(clientsModel as any, 'generateApiKeyObj').mockReturnValue(testVar_apiKey); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); const testResult: ClientsCreateResponseDTO = { @@ -137,7 +139,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { }) test('Repository process fn updateFlag(), result: "SUCCESS"', async () => { - const testParam_id = mockId.clients.valid[0]; + const testParam_id = mockValidClientId; const testParam_dto: ClientsFlagUpdateDTO = { flag: Flag.WARNING }; @@ -159,7 +161,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { }) test('Repository process fn updateFlag(), result: null', async () => { - const testParam_id = mockId.clients.invalid[0]; + const testParam_id = mockId.clients.invalid[0] as ClientsId; const testParam_dto: ClientsFlagUpdateDTO = { flag: Flag.WARNING }; @@ -175,7 +177,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { }) test('Repository process fn updateStatus(), result: "SUCCESS"', async () => { - const testParam_id = mockId.clients.valid[0]; + const testParam_id = mockValidClientId; const testParam_data: Partial = { status: ApiKeyStatus.DISABLED }; @@ -208,7 +210,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { describe('Route: PUT/status/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID' as ClientsId; const testParam_dto: ClientsStatusUpdateDTO = { status: ApiKeyStatus.EXPIRED }; @@ -304,7 +306,7 @@ describe('Integration-tests (repository), priority: entity Clients', () => { describe('Route: PUT/status/:id', () => { test('Params: , validator: fn requirePayload() by undefined', async () =>{ - const testParam_id = mockId.clients.valid[0]; + const testParam_id = mockValidClientId; const testParam_dto = undefined; const testError = structuredClone(mockError); diff --git a/backend/tests/integration-tests/repositories/feedback-rating.integration.test.ts b/backend/tests/integration-tests/repositories/feedback-rating.integration.test.ts index 56692be..880a181 100644 --- a/backend/tests/integration-tests/repositories/feedback-rating.integration.test.ts +++ b/backend/tests/integration-tests/repositories/feedback-rating.integration.test.ts @@ -6,13 +6,15 @@ import { runMigrations } from '../../db-migrations.setup'; import { default as mockId } from "../../mock-data/id.mock-data.json"; import { FeedbackRatingExtendedResponseDTO, FeedbackRatingResponseDTO } from "../../../src/dtos/feedback-rating.dto"; import { ErrorStatusCodes } from "../../../src/utils/errorStatusCodes.utils"; +import { DBTestData } from "../../db-data.setup"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ authAdmin: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next()) })); jest.mock('../../../src/middleware/auth.client.middleware', () => ({ authClient: jest.fn(() => (req: Request, res: Response, next: NextFunction) => { - (req as any).apiClients = { client_id: mockId.clients.valid[0] }; + (req as any).apiClients = { client_id: testValidClientId }; next(); }) })); @@ -21,10 +23,10 @@ jest.mock('../../../src/middleware/maintenance.middleware', () => ({ })); import app from '../../../src/app'; -import { DBTestData } from "../../db-data.setup"; jest.setTimeout(60000); +const testValidClientId = mockId.clients.valid[0] as ClientsId; const testTimestamp = '2025-01-01T14:00:09.000Z'; describe('Integration-tests (repository), priority: entity FeedbackRating', () => { @@ -53,7 +55,7 @@ describe('Integration-tests (repository), priority: entity FeedbackRating', () = describe('Testing valid fn calls', () => { test('Repository process fn findById(), result: "SUCCESS"', async () => { - const testParam_id = mockId.clients.valid[0]; + const testParam_id = testValidClientId; const testResult: FeedbackRatingExtendedResponseDTO | null = { client_id: testParam_id, count: dbData_FeedbackRating[1], @@ -89,7 +91,7 @@ describe('Integration-tests (repository), priority: entity FeedbackRating', () = test('Repository process fn findAll(), result: "SUCCESS"', async () => { const testResult: FeedbackRatingExtendedResponseDTO[] | null = [ { - client_id: mockId.clients.valid[0], + client_id: testValidClientId, count: dbData_FeedbackRating[1], rating_sum: dbData_FeedbackRating[2], rating_average: 4.2, @@ -116,7 +118,7 @@ describe('Integration-tests (repository), priority: entity FeedbackRating', () = describe('Route: GET/id/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID' as ClientsId; const testError = { type: 'field', value: testParam_id, diff --git a/backend/tests/integration-tests/repositories/feedback.integration.test.ts b/backend/tests/integration-tests/repositories/feedback.integration.test.ts index dee45d9..b12b074 100644 --- a/backend/tests/integration-tests/repositories/feedback.integration.test.ts +++ b/backend/tests/integration-tests/repositories/feedback.integration.test.ts @@ -14,12 +14,11 @@ import router from '../../../src/routes/feedback.route'; import feedbackRatingService from '../../../src/services/feedback-rating.service'; import feedbackRepository from '../../../src/repositories/feedback.repository'; import feedbackService from '../../../src/services/feedback.service'; -import { DBQueryErrorException } from '../../../src/utils/exceptions/db.exception'; +import { DBConstraintErrorException, DBQueryErrorException } from '../../../src/utils/exceptions/db.exception'; import feedbackRatingRepository from '../../../src/repositories/feedback-rating.repository'; - -const testValidClientsId = mockId.clients.valid[0]; -const testValidUsersId = mockId.users.valid[0]; -const testTimestamp = '2025-01-01T14:00:08.000Z'; +import { ClientsId } from '../../../src/repositories/interfaces/clients.entity.interface'; +import { UsersId } from '../../../src/repositories/interfaces/users.entity.interface'; +import { FeedbackId } from '../../../src/repositories/interfaces/feedback.entity.interface'; jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ authAdmin: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next()) @@ -39,6 +38,11 @@ jest.mock('../../../src/middleware/observe.middleware.ts', () => ({ jest.setTimeout(60000); +const testValidFeedbackId = mockId.feedback.valid[0] as FeedbackId; +const testValidClientId = mockId.clients.valid[0] as ClientsId; +const testValidUserId = mockId.users.valid[0] as UsersId; +const testTimestamp = '2025-01-01T14:00:08.000Z'; + describe('Integration-tests (repository), priority: entity Feedback', () => { let dbTestSetup: DBTestSetup; @@ -70,11 +74,11 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Repository process fn findById(), result: "SUCCESS"', async () => { const app = createTestApp([], router, apiUrl); - const testParam_id = mockId.feedback.valid[0]; + const testParam_id = testValidFeedbackId; const testResult: FeedbackResponseDTO | null = { feedback_id: testParam_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, rating: dbData_Feedback[2], term_accepted: dbData_Feedback[3], message: dbData_Feedback[4], @@ -108,8 +112,8 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Repository process fn upsertInTa(), result: new Feedback on existing FeedbackRating', async () => { const app = createTestApp([ - MockUtils.injectTestClientId(mockId.clients.valid[0]), - MockUtils.injectTestUserId(mockId.users.valid[1]) + MockUtils.injectTestClientId(testValidClientId), + MockUtils.injectTestUserId(mockId.users.valid[1] as UsersId) ], router, apiUrl); const email = dbTestData.getUsersInsertSql().values[7]; // name, 2nd insert-row const testParam_dto: FeedbackRequestCreateDTO = { @@ -123,9 +127,9 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); const testResult: FeedbackResponseDTO = { - feedback_id: mockId.feedback.new[0], - client_id: testValidClientsId, - user_id: mockId.users.valid[1], + feedback_id: mockId.feedback.new[0] as FeedbackId, + client_id: testValidClientId, + user_id: mockId.users.valid[1] as UsersId, rating: testParam_dto.rating, rating_average_new: mockResult_average_rating, term_accepted: testParam_dto.term_accepted, @@ -144,8 +148,8 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Repository process fn upsertInTa(), result: new Feedback & FeedbackRating', async () => { const app = createTestApp([ - MockUtils.injectTestClientId(mockId.clients.valid[1]), - MockUtils.injectTestUserId(mockId.users.valid[0]) + MockUtils.injectTestClientId(mockId.clients.valid[1] as ClientsId), + MockUtils.injectTestUserId(testValidUserId) ], router, apiUrl); const email = dbTestData.getUsersInsertSql().values[7]; const testParam_dto: FeedbackRequestCreateDTO = { @@ -158,9 +162,9 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); const testResult: FeedbackResponseDTO = { - feedback_id: mockId.feedback.new[0], - client_id: mockId.clients.valid[1], - user_id: mockId.users.valid[0], + feedback_id: mockId.feedback.new[0] as FeedbackId, + client_id: mockId.clients.valid[1] as ClientsId, + user_id: testValidUserId, rating: testParam_dto.rating, rating_average_new: testParam_dto.rating, term_accepted: testParam_dto.term_accepted, @@ -174,7 +178,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { .send(testParam_dto); const testResponse_FeedbackRating = - await feedbackRatingService.getExtendedFeedbackRatingById(mockId.clients.valid[1]); + await feedbackRatingService.getExtendedFeedbackRatingById(mockId.clients.valid[1] as ClientsId); expect(testResponse_Feedback.statusCode).toBe(200); expect(testResponse_Feedback.body).toMatchObject(testResult); @@ -184,15 +188,14 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Repository process fn upsertInTa(), result: update existing Feedback & FeedbackRating', async () => { const app = createTestApp([ - MockUtils.injectTestClientId(mockId.clients.valid[0]), - MockUtils.injectTestUserId(mockId.users.valid[0]) + MockUtils.injectTestClientId(testValidClientId), + MockUtils.injectTestUserId(testValidUserId) ], router, apiUrl); const email = dbTestData.getUsersInsertSql().values[1]; const testParam_dto: FeedbackRequestCreateDTO = { user_email: email, rating: 2, term_accepted: true, - message: 'test-feedback-message-updated-client[0]' }; // Test data FeedbackRating => position (count): [1], position (rating_sum): [2] const ratingDifference = testParam_dto.rating - dbTestData.getFeedbackInsertSql().values[2]; @@ -203,24 +206,25 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp_update); const testResult: FeedbackResponseDTO = { - feedback_id: mockId.feedback.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + feedback_id: testValidFeedbackId, + client_id: testValidClientId, + user_id: testValidUserId, rating: testParam_dto.rating, rating_average_new: mockResult_average_rating, term_accepted: testParam_dto.term_accepted, - message: testParam_dto.message, last_modified: testTimestamp_update, - created_on: testTimestamp + created_on: testTimestamp, }; await dbTestSetup.addTestData(); + // Update reviewed_on from NULL to value, otherwise WHERE clause sets blocked:true => throw exception. + await feedbackService.updateFeedbackReview(testValidFeedbackId); const testResponse_Feedback = await request(app) .post(`${apiUrl}/create`) .send(testParam_dto); const testResponse_FeedbackRating = - await feedbackRatingService.getExtendedFeedbackRatingById(mockId.clients.valid[0]); + await feedbackRatingService.getExtendedFeedbackRatingById(testValidClientId); expect(testResponse_Feedback.statusCode).toBe(200); expect(testResponse_Feedback.body).toMatchObject(testResult); @@ -230,13 +234,13 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Repository process fn upsertInTa(), result: Exception on Feedback + Rollback', async () => { const testParam_dto: FeedbackCreateDTO = { - client_id: mockId.clients.valid[1], - user_id: mockId.users.valid[0], + client_id: mockId.clients.valid[1] as ClientsId, + user_id: testValidUserId, rating: 5, term_accepted: true, message: 'test-feedback-message-new-client[1]' }; - const mockError = 'Feedback-upsert-mock-error'; + const mockError = 'feedback-upsert-mock-error'; jest.spyOn(CommonUtils, 'logError').mockImplementation(); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); @@ -247,15 +251,15 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { .rejects.toThrow(new DBQueryErrorException(mockError)); const testResponse_Feedback = - await feedbackService.searchFeedbackEntriesByFilter({client_id: mockId.clients.valid[1]}); + await feedbackService.searchFeedbackEntriesByFilter({client_id: mockId.clients.valid[1] as ClientsId}); expect(testResponse_Feedback).toBe(null); // rollback => no insert }) test('Repository process fn upsertInTa(), result: Exception on FeedbackRating + Rollback', async () => { const testParam_dto: FeedbackCreateDTO = { - client_id: mockId.clients.valid[1], - user_id: mockId.users.valid[0], + client_id: mockId.clients.valid[1] as ClientsId, + user_id: testValidUserId, rating: 5, term_accepted: true, message: 'test-feedback-message-new-client[1]' @@ -267,9 +271,9 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { await dbTestSetup.addTestData(); const testResponse_Feedback = - await feedbackService.searchFeedbackEntriesByFilter({client_id: mockId.clients.valid[1]}); + await feedbackService.searchFeedbackEntriesByFilter({client_id: mockId.clients.valid[1] as ClientsId}); const testResponse_FeedbackRating = - await feedbackRatingService.getExtendedFeedbackRatingById(mockId.clients.valid[1]); + await feedbackRatingService.getExtendedFeedbackRatingById(mockId.clients.valid[1] as ClientsId); await expect(() => feedbackService.createFeedback(testParam_dto)) .rejects.toThrow(new DBQueryErrorException(mockError)); @@ -278,9 +282,45 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { expect(testResponse_FeedbackRating).toBe(null); }) + test('Repository process fn upsertInTa(), result: Exception on `Blocked`', async () => { + const testParam_dto: FeedbackCreateDTO = { + client_id: mockId.clients.valid[1] as ClientsId, + user_id: testValidUserId, + rating: 3, + term_accepted: true, + message: 'test-feedback-message-new-client[1]' + }; + const testFeedbackId = testValidFeedbackId; + const testResult: FeedbackResponseDTO | null = { + feedback_id: testFeedbackId, + client_id: testValidClientId, + user_id: testValidUserId, + rating: dbData_Feedback[2], + term_accepted: dbData_Feedback[3], + message: dbData_Feedback[4], + last_modified: testTimestamp, + created_on: testTimestamp, + blocked: true + }; + const mockError = 'support-constraint-feedback'; + + jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(feedbackRepository, 'upsertInTa').mockResolvedValue(testResult); + + await dbTestSetup.addTestData(); + await expect(() => feedbackService.createFeedback(testParam_dto)) + .rejects.toThrow(new DBConstraintErrorException(mockError)); + + const testCompareResult = await feedbackService.getFeedbackById(testFeedbackId); + + expect(testCompareResult?.rating).not.toBe(testParam_dto.rating); + expect(testCompareResult?.message).not.toBe(testParam_dto.message); + expect(testCompareResult?.last_modified).toBe(testResult.last_modified); + }) + test('Repository process fn updateReview(), result: "SUCCESS"', async () => { const app = createTestApp([], router, apiUrl); - const testParam_id = mockId.feedback.valid[0]; + const testParam_id = testValidFeedbackId; const mockTimestamp = '2026-01-01T14:00:08.000Z'; // Test without changing created_on this time to see how .spyOn works with multiple calls in process. jest.spyOn(CommonUtils, "getTimestampUTC") @@ -291,8 +331,8 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { const testResult: FeedbackResponseDTO = { feedback_id: testParam_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, rating: dbData_Feedback[2], term_accepted: dbData_Feedback[3], message: dbData_Feedback[4], @@ -330,7 +370,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { describe('Route: GET/id/:id', () => { test('Params: , validator: fn isInt() by value as string', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-id-number'; const testError = structuredClone(mockError); testError['value'] = testParam_id; @@ -345,7 +385,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { describe('Route: PUT/update/review/:id', () => { test('Params: , validator: fn isInt() by value as string', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-id-number'; const testError = structuredClone(mockError); testError['value'] = testParam_id; @@ -380,7 +420,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Params: , validator: fn isUUID() by invalid value', async () => { let testParam_dto: FeedbackFilterDTO = { - client_id: [mockId.clients.valid[0], 'invalid-id'] + client_id: [testValidClientId, 'invalid-id'] as ClientsId[] } const testError = [{ type: 'field', @@ -400,7 +440,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { test('Params: , validator: fn isUUID() by invalid value', async () => { let testParam_dto: FeedbackFilterDTO = { - user_id: ['invalid-id-0', mockId.clients.valid[0], 'invalid-id-1'] + user_id: ['invalid-id-0', testValidUserId, 'invalid-id-1'] as UsersId[] } const testError = [ { @@ -542,8 +582,8 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { let testData: Partial; beforeEach(() => { app = createTestApp([ - MockUtils.injectTestClientId(mockId.clients.valid[0]), - MockUtils.injectTestUserId(mockId.users.valid[0]) + MockUtils.injectTestClientId(testValidClientId), + MockUtils.injectTestUserId(testValidUserId) ], router, apiUrl); testData = { user_email: 'max.muster@test.com', @@ -700,8 +740,8 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { let app: any; beforeEach(() => { app = createTestApp([ - MockUtils.injectTestClientId(mockId.clients.valid[0]), - MockUtils.injectTestUserId(mockId.users.valid[0]) + MockUtils.injectTestClientId(testValidClientId), + MockUtils.injectTestUserId(testValidUserId) ], router, apiUrl); }) diff --git a/backend/tests/integration-tests/repositories/meta.integration.test.ts b/backend/tests/integration-tests/repositories/meta.integration.test.ts index a845d29..a7f028c 100644 --- a/backend/tests/integration-tests/repositories/meta.integration.test.ts +++ b/backend/tests/integration-tests/repositories/meta.integration.test.ts @@ -1,4 +1,4 @@ -import { Maintenance, Meta } from './../../../src/repositories/interfaces/meta.entity.interface'; +import { Maintenance, Meta, MetaId } from './../../../src/repositories/interfaces/meta.entity.interface'; import { NextFunction, Request, Response } from "express"; import { DBTestSetup } from "../../db-container.setup"; import { runMigrations } from '../../db-migrations.setup'; @@ -12,6 +12,7 @@ import { CommonExceptionMessage } from '../../../src/utils/enums/common-exceptio import { EnvMode } from '../../../src/utils/enums/env-mode.enum'; import { DemoMode } from '../../../src/utils/enums/demo-mode.enum'; import { default as mockId } from "../../mock-data/id.mock-data.json"; +import { secrets } from '../../../src/utils/secrets.utils'; jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ authAdmin: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next()) @@ -23,6 +24,8 @@ jest.mock('../../../src/middleware/maintenance.middleware', () => ({ import app from '../../../src/app'; jest.setTimeout(60000); + +const testValidMetaId = mockId.meta.valid[0] as MetaId; const testTimestamp = '2025-01-01T14:00:01.000Z'; describe('Integration-tests (repository), priority: entity Meta', () => { @@ -37,7 +40,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { await runMigrations('meta.integration.test.ts'); apiUrl = '/api/v1/meta'; mockResult = { - id: 1, + id: testValidMetaId, app: 'support', author: 'yqni13', build_on: testTimestamp, @@ -65,7 +68,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { }); test('Repository process fn findById(), result: "SUCCESS"', async () => { - const testParam_id = 1; + const testParam_id = testValidMetaId; const testResult: Meta = { id: testParam_id, app: "support", @@ -84,7 +87,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { await dbTestSetup.addTestData(); const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(200); expect(testResponse.body).toMatchObject(testResult) @@ -93,7 +96,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { test('Repository process fn findByName(), result: "SUCCESS"', async () => { const testParam_name = "support"; const testResult: Meta = { - id: 1, + id: testValidMetaId, app: testParam_name, author: "yqni13", build_on: testTimestamp, @@ -110,7 +113,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { await dbTestSetup.addTestData(); const testResponse = await request(app) - .get(`${apiUrl}/by-name/${testParam_name}`); + .get(`${apiUrl}/name/${testParam_name}`); expect(testResponse.statusCode).toBe(200); expect(testResponse.body).toMatchObject(testResult) @@ -118,7 +121,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { test('Repository process fn findAll(), result: "SUCCESS"', async () => { const testResult: Meta[] = [{ - id: 1, + id: testValidMetaId, app: "support", author: "yqni13", build_on: testTimestamp, @@ -142,7 +145,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { }) test('Repository process fn update(), result: "SUCCESS"', async () => { - const testParam_id = 1; + const testParam_id = testValidMetaId; const testParam_dto: MetaUpdateDTO = { app: 'support', author: 'yqni13', @@ -193,7 +196,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { }) test('Repository process fn updateMaintenance(), result: "SUCCESS"', async () => { - const testParam_id = mockId.meta.valid[0]; + const testParam_id = testValidMetaId; const testParam_data = { maintenance_mode: MaintenanceMode.E013 }; // Mock Utils generated timeStamp for easy comparison. @@ -224,6 +227,8 @@ describe('Integration-tests (repository), priority: entity Meta', () => { const testResult = { app_version: '0.0.1' }; + jest.replaceProperty(secrets, 'APP_VERSION', '0.0.1'); + await dbTestSetup.addTestData(); const testResponse = await request(app) .post('/api/v1/test/demo') @@ -254,7 +259,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { describe('All routes, priority: express-validators, location: ', () => { - describe('Route: GET/by-id:id', () => { + describe('Route: GET/id:id', () => { test('Params: , validator: fn isInt() by string', async () => { const testParam_id = 'invalid_test_id'; @@ -267,7 +272,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { }; const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(ErrorStatusCodes.InvalidPropertiesException); expect(testResponse.body.headers.data).toEqual([testError]); @@ -365,7 +370,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { const testedParams = Object.keys(testData) as (keyof typeof testData)[]; test.each(testedParams)('Params: <%s>, validator: fn notEmpty() by undefined', async (invalidParam) => { - const testParam_id = 1; + const testParam_id = testValidMetaId; let mockParam_dto = structuredClone(testData); delete mockParam_dto[invalidParam]; const testError = structuredClone(mockError); @@ -397,7 +402,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { describe('Route: PUT/info/:id', () => { test('Params: , validator: fn requirePayload() by undefined', async () =>{ - const testParam_id = 1; + const testParam_id = testValidMetaId; const testParam_dto = undefined; const testError = structuredClone(mockError); @@ -415,7 +420,7 @@ describe('Integration-tests (repository), priority: entity Meta', () => { describe('Route: PUT/maintenance/:id', () => { test('Params: , validator: fn requirePayload() by undefined', async () =>{ - const testParam_id = mockId.meta.valid[0]; + const testParam_id = testValidMetaId; const testParam_dto = undefined; const testError = structuredClone(mockError); diff --git a/backend/tests/integration-tests/repositories/rate-limits.integration.test.ts b/backend/tests/integration-tests/repositories/rate-limits.integration.test.ts index 8471bf2..f6a60ec 100644 --- a/backend/tests/integration-tests/repositories/rate-limits.integration.test.ts +++ b/backend/tests/integration-tests/repositories/rate-limits.integration.test.ts @@ -12,11 +12,16 @@ import * as CommonUtils from '../../../src/utils/common.utils'; import rateLimitsService from "../../../src/services/rate-limits.service"; import demoLimitsService from "../../../src/services/demo-limits.service"; import { DemoLimitsCountDTO, DemoLimitsResponseDTO } from "../../../src/dtos/demo-limits.dto"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { RateLimitsId } from "../../../src/repositories/interfaces/rate-limits.entity.interface"; +import { DemoLimitsId } from "../../../src/repositories/interfaces/demo-limits.entity.interface"; jest.setTimeout(60000); -const testValidClientsId = mockId.clients.valid[0]; -const testValidUsersId = mockId.users.valid[0]; +const testValidRateLimitId = mockId.rate_limits.valid[0] as RateLimitsId; +const testValidClientId = mockId.clients.valid[0] as ClientsId; +const testValidUserId = mockId.users.valid[0] as UsersId; describe('Integration-tests (repository), priority: entity RateLimits', () => { @@ -40,7 +45,7 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { test('Repository process fn getRateLimitCount(), params: , result: "SUCCESS"', async () => { const dto: RateLimitsCountDTO = { - client_id: mockId.clients.valid[0], + client_id: testValidClientId, day: '2025-01-01' }; @@ -66,8 +71,8 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { test('Repository process fn createRateLimit(), result: "SUCCESS"', async () => { const testTimestamp = '2025-01-02T14:00:05.000Z'; const mockParam_dto: RateLimitsCreateDTO = { - client_id: testValidClientsId, - user_id: testValidUsersId + client_id: testValidClientId, + user_id: testValidUserId }; const dateUTC = CommonUtils.getDateUTC(new Date(testTimestamp)); @@ -77,9 +82,9 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { await dbTestSetup.addTestData(); const testResponse = await rateLimitsService.createRateLimit(mockParam_dto); const mockResponse: RateLimitsResponseDTO = { - rate_limit_id: 2, - client_id: testValidClientsId, - user_id: testValidUsersId, + rate_limit_id: mockId.rate_limits.new[0] as RateLimitsId, + client_id: testValidClientId, + user_id: testValidUserId, day: '2025-01-02', count: 1, last_modified: testTimestamp @@ -92,8 +97,8 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { test('Repository process fn updateRateLimit(), result: "SUCCESS"', async () => { const testTimestamp = '2025-01-01T22:22:22.000Z'; const dto: RateLimitsUpdateDTO = { - client_id: testValidClientsId, - user_id: testValidUsersId + client_id: testValidClientId, + user_id: testValidUserId }; jest.spyOn(CommonUtils, 'getDateUTC').mockReturnValue(testTimestamp); @@ -102,9 +107,9 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { await dbTestSetup.addTestData(); const testResponse = await rateLimitsService.updateRateLimit(dto); const mockResponse: RateLimitsResponseDTO = { - rate_limit_id: 1, - client_id: testValidClientsId, - user_id: testValidUsersId, + rate_limit_id: testValidRateLimitId, + client_id: testValidClientId, + user_id: testValidUserId, day: '2025-01-01', count: 2, last_modified: testTimestamp @@ -117,8 +122,8 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { test('Repository process fn updateRateLimit(), result: null', async () => { const testTimestamp = '2025-01-02T22:22:22.000Z'; const dto: RateLimitsUpdateDTO = { - client_id: testValidClientsId, - user_id: testValidUsersId + client_id: testValidClientId, + user_id: testValidUserId }; jest.spyOn(CommonUtils, 'getDateUTC').mockReturnValue(testTimestamp); @@ -156,7 +161,7 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { await dbTestSetup.addTestData(); const testResponse = await demoLimitsService.createDemoLimit(); const mockResponse: DemoLimitsResponseDTO = { - demo_limit_id: 2, + demo_limit_id: 2 as DemoLimitsId, day: '2025-01-02', count: 1, last_modified: testTimestamp @@ -175,7 +180,7 @@ describe('Integration-tests (repository), priority: entity RateLimits', () => { await dbTestSetup.addTestData(); const testResponse = await demoLimitsService.updateDemoLimit(); const mockResponse: DemoLimitsResponseDTO = { - demo_limit_id: 1, + demo_limit_id: 1 as DemoLimitsId, day: '2025-01-01', count: 2, last_modified: testTimestamp diff --git a/backend/tests/integration-tests/repositories/tickets.integration.test.ts b/backend/tests/integration-tests/repositories/tickets.integration.test.ts index e78d98e..2a1119d 100644 --- a/backend/tests/integration-tests/repositories/tickets.integration.test.ts +++ b/backend/tests/integration-tests/repositories/tickets.integration.test.ts @@ -21,9 +21,13 @@ import ticketsModel from "../../../src/models/tickets.model"; import { CloudService } from "../../../src/services/cloud.service"; import { TicketOption } from "../../../src/utils/enums/ticket-option.enum"; import { DeviceOption } from "../../../src/utils/enums/device-option.enum"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface"; -const testValidClientsId = mockId.clients.valid[0]; -const testValidUsersId = mockId.users.valid[0]; +const testValidTicketId = mockId.tickets.valid[0] as TicketsId; +const testValidClientId = mockId.clients.valid[0] as ClientsId; +const testValidUserId = mockId.users.valid[0] as UsersId; const testTimestamp = '2025-01-01T14:00:04.000Z'; jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ @@ -31,13 +35,13 @@ jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ })); jest.mock('../../../src/middleware/auth.client.middleware', () => ({ authClient: jest.fn(() => (req: Request, res: Response, next: NextFunction) => { - (req as any).apiClients = { client_id: testValidClientsId }; + (req as any).apiClients = { client_id: testValidClientId }; next(); }) })); jest.mock('../../../src/middleware/auth.user.middleware', () => ({ authUser: jest.fn(() => (req: Request, res: Response, next: NextFunction) => { - (req as any).apiUsers = { user_id: testValidUsersId }; + (req as any).apiUsers = { user_id: testValidUserId }; next(); }) })); @@ -58,7 +62,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { let apiUrl: string; const testClientsName = 'TESTCLIENT'; const testUsersEmail = 'max.mustermann@yqni13.com'; - const testNewParam_ticket_id = mockId.tickets.new[0]; + const testNewParam_ticket_id = mockId.tickets.new[0] as TicketsId; beforeAll(async () => { dbTestSetup = new DBTestSetup(); await dbTestSetup.init(); @@ -79,12 +83,12 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { describe('Testing valid fn calls', () => { test('Repository process fn findById(), result: "SUCCESS"', async () => { - const testParam_id = mockId.tickets.valid[0]; + const testParam_id = testValidTicketId; const testResult: TicketsResponseExtendedDTO = { ticket_id: testParam_id, - client_id: testValidClientsId, + client_id: testValidClientId, client_name: testClientsName, - user_id: testValidUsersId, + user_id: testValidUserId, user_email: testUsersEmail, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, @@ -101,7 +105,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { await dbTestSetup.addTestData(); const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(200); expect(testResponse.body).toMatchObject(testResult); @@ -110,9 +114,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { test('Repository process fn findAll(), result: "SUCCESS"', async () => { const testResult: TicketsResponseDTO[] = [ { - ticket_id: mockId.tickets.valid[0], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: testValidTicketId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -126,9 +130,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { created_on: testTimestamp }, { - ticket_id: mockId.tickets.valid[1], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -152,7 +156,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { test('Repository process fn findByFilter(), params: result: null', async () => { const testParam_dto: TicketsFilterDTO = { - client_id: mockId.tickets.invalid[0] + client_id: mockId.clients.invalid[0] as ClientsId }; const testResult: TicketsResponseDTO[] | null = null; @@ -167,14 +171,14 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { test('Repository process fn findByFilter(), params: result: "SUCCESS"', async () => { const testParam_dto: TicketsFilterDTO = { - user_id: [testValidUsersId, mockId.tickets.invalid[0]], + user_id: [testValidUserId, mockId.users.invalid[0]] as UsersId[], status: TicketStatus.ISSUED }; const testResult: TicketsResponseDTO[] = [ { - ticket_id: mockId.tickets.valid[0], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: testValidTicketId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -188,9 +192,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { created_on: testTimestamp }, { - ticket_id: mockId.tickets.valid[1], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -221,9 +225,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }; const testResult: TicketsResponseDTO[] = [ { - ticket_id: mockId.tickets.valid[0], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: testValidTicketId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -237,9 +241,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { created_on: testTimestamp }, { - ticket_id: mockId.tickets.valid[1], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -270,9 +274,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }; const testResult: TicketsResponseDTO[] = [ { - ticket_id: mockId.tickets.valid[0], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: testValidTicketId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -286,9 +290,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { created_on: testTimestamp }, { - ticket_id: mockId.tickets.valid[1], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -319,9 +323,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }; const testResult: TicketsResponseDTO[] = [ { - ticket_id: mockId.tickets.valid[0], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: testValidTicketId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -335,9 +339,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { created_on: testTimestamp }, { - ticket_id: mockId.tickets.valid[1], - client_id: testValidClientsId, - user_id: testValidUsersId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -393,8 +397,8 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { const testResult: TicketsResponseDTO = { ticket_id: testNewParam_ticket_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: testParam_dto.title, @@ -434,8 +438,8 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { const testResult: TicketsResponseDTO = { ticket_id: testNewParam_ticket_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: testParam_dto.title, @@ -492,8 +496,8 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { const testResult: TicketsResponseDTO = { ticket_id: testNewParam_ticket_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: testParam_dto.title, @@ -520,7 +524,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }) test('Repository process fn update() without resource_paths, result: "SUCCESS"', async () => { - const testParam_id = mockId.tickets.valid[1]; + const testParam_id = mockId.tickets.valid[1] as TicketsId; const testParam_dto: TicketsUpdateDTO = { status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, @@ -541,8 +545,8 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { info_os: 'Android 15', info_device: DeviceOption.MOBILE, ticket_id: testParam_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, last_modified: mockTimestampNoPaths, created_on: mockTimestampNoPaths }; @@ -557,7 +561,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }) test('Repository process fn update() with resource_paths, result: "SUCCESS"', async () => { - const testParam_id = mockId.tickets.valid[0]; + const testParam_id = testValidTicketId; const testParam_dto: TicketsUpdateDTO = { status: TicketStatus.ACTIVE, option: TicketOption.BUG, @@ -578,8 +582,8 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { info_os: 'Windows 11', info_device: DeviceOption.COMPUTER, ticket_id: testParam_id, - client_id: testValidClientsId, - user_id: testValidUsersId, + client_id: testValidClientId, + user_id: testValidUserId, last_modified: testTimestamp, created_on: testTimestamp }; @@ -607,7 +611,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }) test('Repository process fn delete() with existing data for resource_paths, result: "SUCCESS"', async () => { - const testParam_id = mockId.tickets.valid[0]; + const testParam_id = testValidTicketId; const testResult = true; jest.spyOn(ticketsModel, 'isPermittedToDelete').mockReturnValue(true); @@ -769,15 +773,15 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { }; }) - describe('Route: GET/by-id/:id', () => { + describe('Route: GET/id/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID'; const testError = structuredClone(mockError); testError['value'] = testParam_id; const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(ErrorStatusCodes.InvalidPropertiesException); expect(testResponse.body.headers.data).toEqual([testError]); @@ -787,7 +791,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { describe('Route: PUT/update/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID'; const testParam_dto: TicketsUpdateDTO = { status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, @@ -810,7 +814,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { describe('Route: DELETE/delete/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID'; const testError = structuredClone(mockError); testError['value'] = testParam_id; @@ -840,7 +844,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { test('Params: , validator: fn isUUID() by invalid value', async () => { let testParam_dto: TicketsFilterDTO = { - client_id: [mockId.clients.valid[0], 'invalid-id'] + client_id: [testValidClientId, 'invalid-id'] as ClientsId[] } const testError = [{ type: 'field', @@ -860,7 +864,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { test('Params: , validator: fn isUUID() by invalid value', async () => { let testParam_dto: TicketsFilterDTO = { - user_id: ['invalid-id-0', mockId.clients.valid[0], 'invalid-id-1'] + user_id: ['invalid-id-0', testValidUserId, 'invalid-id-1'] as UsersId[] } const testError = [ { @@ -1036,10 +1040,10 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { describe('Route: PUT/update/:id', () => { - let testParam_id: string; + let testParam_id: TicketsId; let testData: Partial; beforeEach(() => { - testParam_id = mockId.tickets.invalid[0]; + testParam_id = mockId.tickets.invalid[0] as TicketsId; testData = { status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, @@ -1218,7 +1222,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { describe('Route: PUT/update/:id', () => { test('Params: , validator: fn hasBodyPayload() by undefined', async () =>{ - const testParam_id = mockId.tickets.valid[0]; + const testParam_id = testValidTicketId; const testParam_dto = undefined; jest.spyOn(CommonUtils, 'logError').mockImplementation(); diff --git a/backend/tests/integration-tests/repositories/users.integration.test.ts b/backend/tests/integration-tests/repositories/users.integration.test.ts index 718e666..2966c96 100644 --- a/backend/tests/integration-tests/repositories/users.integration.test.ts +++ b/backend/tests/integration-tests/repositories/users.integration.test.ts @@ -6,7 +6,7 @@ import { ErrorStatusCodes } from '../../../src/utils/errorStatusCodes.utils'; import { DBTestSetup } from "../../db-container.setup"; import { runMigrations } from '../../db-migrations.setup'; import { UsersUpdateDTO, UsersFilterDTO, UsersResponseDTO, UsersCreateDTO, UsersFlagUpdateDTO } from "../../../src/dtos/users.dto"; -import { Users } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import { UserStatus } from "../../../src/utils/enums/user-status.enum"; import { Flag } from "../../../src/utils/enums/flag.enum"; import { default as mockId } from "../../mock-data/id.mock-data.json"; @@ -23,6 +23,8 @@ jest.mock('../../../src/middleware/maintenance.middleware', () => ({ import app from '../../../src/app'; jest.setTimeout(60000); + +const testValidUserId = mockId.users.valid[0] as UsersId; const testTimestamp = '2025-01-01T14:00:03.000Z'; describe('Integration-tests (repository), priority: entity Users', () => { @@ -48,7 +50,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { describe('Testing valid fn calls', () => { test('Repository process fn findById(), result: "SUCCESS"', async () => { - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; const testResult: UsersResponseDTO = { user_id: testParam_id, email: 'max.mustermann@yqni13.com', @@ -60,7 +62,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { await dbTestSetup.addTestData(); const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(200); expect(testResponse.body).toMatchObject(testResult); @@ -69,7 +71,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { test('Repository process fn findByEmail(), result: "SUCCESS"', async () => { const testParam_email = 'max.mustermann@yqni13.com'; const testResult: UsersResponseDTO = { - user_id: mockId.users.valid[0], + user_id: testValidUserId, email: testParam_email, status: UserStatus.ACTIVE, flag: null, @@ -79,17 +81,17 @@ describe('Integration-tests (repository), priority: entity Users', () => { await dbTestSetup.addTestData(); const testResponse = await request(app) - .get(`${apiUrl}/by-email/${testParam_email}`); + .get(`${apiUrl}/email/${testParam_email}`); expect(testResponse.statusCode).toBe(200); expect(testResponse.body).toMatchObject(testResult); }) test('Repository process fn findAll(), result: "SUCCESS"', async () => { - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; const testResult: Users[] = [ { - user_id: mockId.users.valid[1], + user_id: mockId.users.valid[1] as UsersId, email: 'aurora.otsuki@yqni13.com', status: UserStatus.ACTIVE, flag: null, @@ -136,7 +138,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { status: UserStatus.ACTIVE }; const testResult: UsersResponseDTO[] = [{ - user_id: mockId.users.valid[0], + user_id: testValidUserId, email: 'max.mustermann@yqni13.com', status: UserStatus.ACTIVE, flag: null, @@ -161,7 +163,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { last_modified: ['2024-12-01T10:00:00.000Z', '2025-12-01T14:00:00.000Z'] }; const testResult: UsersResponseDTO[] = [{ - user_id: mockId.users.valid[0], + user_id: testValidUserId, email: 'max.mustermann@yqni13.com', status: UserStatus.ACTIVE, flag: null, @@ -181,7 +183,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { }) test('Repository process fn create(), result: "SUCCESS"', async () => { - const testParam_id = mockId.users.new[0]; + const testParam_id = mockId.users.new[0] as UsersId; const testParam_dto: UsersCreateDTO = { email: 'new-user@test.com' }; @@ -208,7 +210,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { }) test('Repository process fn update(), result: "SUCCESS"', async () => { - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; const testParam_dto: Partial = { email: 'user@test.com', status: UserStatus.ACTIVE, @@ -234,7 +236,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { }) test('Repository process fn updateFlag(), result: "SUCCESS"', async () => { - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; const testParam_dto: UsersFlagUpdateDTO = { flag: Flag.WARNING }; @@ -263,10 +265,10 @@ describe('Integration-tests (repository), priority: entity Users', () => { describe('All routes, priority: express-validators, location: ', () => { - describe('Route: GET/by-id/:id', () => { + describe('Route: GET/id/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID'; const testError = { type: 'field', value: testParam_id, @@ -276,7 +278,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { } const testResponse = await request(app) - .get(`${apiUrl}/by-id/${testParam_id}`); + .get(`${apiUrl}/id/${testParam_id}`); expect(testResponse.statusCode).toBe(ErrorStatusCodes.InvalidPropertiesException); expect(testResponse.body.headers.data).toEqual([testError]); @@ -286,7 +288,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { describe('Route: PUT/update/:id', () => { test('Params: , validator: fn isUUID() by invalid id', async () => { - const testParam_id = 'invalid-id'; + const testParam_id = 'invalid-UUID'; const testParam_dto: UsersUpdateDTO = { email: 'new-user@test.com', status: UserStatus.ACTIVE, @@ -333,7 +335,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { const testedParams = Object.keys(testData) as (keyof typeof testData)[]; test.each(testedParams)('Params: <%s>, validator: fn notEmpty() by undefined', async (invalidParam) => { - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; let mockParam_dto = structuredClone(testData); delete mockParam_dto[invalidParam]; @@ -410,7 +412,7 @@ describe('Integration-tests (repository), priority: entity Users', () => { describe('Route: PUT/update/:id', () => { test('Params: , validator: fn requirePayload() by undefined', async () =>{ - const testParam_id = mockId.users.valid[0]; + const testParam_id = testValidUserId; const testParam_dto = undefined; const testError = structuredClone(mockError); diff --git a/backend/tests/mock-data/id.mock-data.json b/backend/tests/mock-data/id.mock-data.json index 8947320..456f015 100644 --- a/backend/tests/mock-data/id.mock-data.json +++ b/backend/tests/mock-data/id.mock-data.json @@ -1,7 +1,8 @@ { "clients": { "invalid": [ - "c34ad3f7-e263-42a0-bc09-b2d9dd2598c6" + "c34ad3f7-e263-42a0-bc09-b2d9dd2598c6", + "3fd1c826-4c30-42f1-becf-5389575f0d2e" ], "new": [ "56a7a3f5-e2f4-4040-bb00-f194f8853d62" @@ -40,17 +41,18 @@ 0 ], "new": [ - 3, + 2, 4 ], "valid": [ 1, - 2 + 3 ] }, "tickets": { "invalid": [ - "b284d72d-1000-412c-8a7f-bbc9f9b5113d" + "b284d72d-1000-412c-8a7f-bbc9f9b5113d", + "e3dedcd2-bab4-4f05-b592-abdceca708ac" ], "new": [ "6404a514-602b-43de-b4eb-919c83222645" @@ -62,7 +64,8 @@ }, "users": { "invalid": [ - "3c11e85a-5377-4b93-8064-e07e27c643ab" + "3c11e85a-5377-4b93-8064-e07e27c643ab", + "fda3c5e1-3b09-4c49-8f0d-d495a2503a43" ], "new": [ "73b29215-fb40-4b40-904a-2dfe4a448426" diff --git a/backend/tests/unit-tests/middleware/auth.admin.middleware.test.ts b/backend/tests/unit-tests/middleware/auth.admin.middleware.test.ts index 2843c74..dd7ba37 100644 --- a/backend/tests/unit-tests/middleware/auth.admin.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/auth.admin.middleware.test.ts @@ -15,7 +15,7 @@ describe('Unit-tests (middleware), priority: fn authAdmin()', () => { describe('Testing valid fn calls', () => { test('Verfiy admin, params: valid ', async () => { - const mockApiKey = secrets.ADMIN_API.trim(); + const mockApiKey = secrets.ADMIN_KEY.trim(); req.header.mockReturnValue(mockApiKey); // middleware == factory fn returning express fn => fn(req, res, next) diff --git a/backend/tests/unit-tests/middleware/auth.client.middleware.test.ts b/backend/tests/unit-tests/middleware/auth.client.middleware.test.ts index 27c0029..f90c782 100644 --- a/backend/tests/unit-tests/middleware/auth.client.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/auth.client.middleware.test.ts @@ -1,5 +1,5 @@ import { ForbiddenApiKeyException, MissingApiKeyException } from './../../../src/utils/exceptions/auth.exception'; -import { Clients } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import clientsService from "../../../src/services/clients.service"; import { default as mockId } from "../../mock-data/id.mock-data.json"; import { secrets } from "../../../src/utils/secrets.utils"; @@ -11,6 +11,7 @@ import { ErrorStatusCodes } from "../../../src/utils/errorStatusCodes.utils"; describe('Unit-tests (middleware), priority: fn authClient()', () => { + const mockValidClientId = mockId.clients.valid[0] as ClientsId; const mockTimestamp = '2025-01-01T14:00:00.000Z'; const req: any = { header: jest.fn() }; const res: any = {}; @@ -24,7 +25,7 @@ describe('Unit-tests (middleware), priority: fn authClient()', () => { test('Verfiy client, params: valid ', async () => { const mockApiKey = 'valid_api_key'; const mockClient: Clients = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, name: 'valid_clients_test_name', api_key_hash: secrets.TEST_APIKEY_HASH, status: ApiKeyStatus.ACTIVE, diff --git a/backend/tests/unit-tests/middleware/auth.user.middleware.test.ts b/backend/tests/unit-tests/middleware/auth.user.middleware.test.ts index dea99fd..bbd6020 100644 --- a/backend/tests/unit-tests/middleware/auth.user.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/auth.user.middleware.test.ts @@ -1,7 +1,7 @@ import { default as mockId } from "../../mock-data/id.mock-data.json"; import * as CommonUtils from "../../../src/utils/common.utils"; import { authUser } from "../../../src/middleware/auth.user.middleware"; -import { Users } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import { UserStatus } from "../../../src/utils/enums/user-status.enum"; import usersService from "../../../src/services/users.service"; import { UsersResponseDTO } from "../../../src/dtos/users.dto"; @@ -10,6 +10,7 @@ import { Flag } from "../../../src/utils/enums/flag.enum"; describe('Unit-tests (middleware), priority: fn authUser()', () => { + const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-01T14:00:00.000Z'; const res: any = {}; const next = jest.fn(); @@ -22,7 +23,7 @@ describe('Unit-tests (middleware), priority: fn authUser()', () => { test('Verfiy user, params: valid for existing user', async () => { const mockUserEmail = 'valid_user@test.com'; const mockUser: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: mockUserEmail, status: UserStatus.ACTIVE, flag: null, @@ -47,7 +48,7 @@ describe('Unit-tests (middleware), priority: fn authUser()', () => { const mockUserEmail = 'non-existing-user@test.com'; const mockUser: Users | null = null; const mockNewUser: UsersResponseDTO = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: mockUserEmail, status: UserStatus.ACTIVE, flag: null, @@ -75,7 +76,7 @@ describe('Unit-tests (middleware), priority: fn authUser()', () => { test('Verify user, error: InvalidUsersException', async () => { const mockUserEmail = 'valid-user@test.com'; const mockUser: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: mockUserEmail, status: UserStatus.ACTIVE, flag: Flag.ERROR, @@ -99,7 +100,7 @@ describe('Unit-tests (middleware), priority: fn authUser()', () => { test('Verify user, error: BlockedUsersException', async () => { const mockUserEmail = 'valid-user@test.com'; const mockUser: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: mockUserEmail, status: UserStatus.BLACKLISTED, flag: Flag.ERROR, diff --git a/backend/tests/unit-tests/middleware/engines.middleware.test.ts b/backend/tests/unit-tests/middleware/engines.middleware.test.ts index 957dafb..ec9847a 100644 --- a/backend/tests/unit-tests/middleware/engines.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/engines.middleware.test.ts @@ -8,23 +8,24 @@ import demoLimitsService from "../../../src/services/demo-limits.service"; import rateLimitsService from "../../../src/services/rate-limits.service"; import { Violation } from "../../../src/utils/enums/violations.enum"; import { default as mockId } from "../../mock-data/id.mock-data.json"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; // Ensure correct type by converting secret to number via unary + operator. import { secrets } from "../../../src/utils/secrets.utils"; describe('Unit-tests (middleware), priority: class RateLimitsEngine', () => { + const mockValidClientId = mockId.clients.valid[0] as ClientsId; + const mockValidUserId = mockId.users.valid[0] as UsersId; + describe('Testing valid fn calls', () => { - let mockValidClientsId: string; - let mockValidUsersId: string; let mockParam_data: RateLimitsData; beforeEach(() => { - mockValidClientsId = mockId.clients.valid[0], - mockValidUsersId = mockId.users.valid[0]; mockParam_data = { - client_id: mockValidClientsId, - user_id: mockValidUsersId + client_id: mockValidClientId, + user_id: mockValidUserId }; }); @@ -57,7 +58,7 @@ describe('Unit-tests (middleware), priority: class RateLimitsEngine', () => { retryAfter: '2025-01-02T00.00.01.000Z', penalty: { type: Violation.CLIENTSFLAG, - id: mockValidClientsId, + id: mockValidClientId, penaltyValue: null } }; @@ -89,7 +90,7 @@ describe('Unit-tests (middleware), priority: class RateLimitsEngine', () => { const engine = new RateLimitsEngine([rule], count); const mockResponse = null; - const testFn = await engine.process({ client_id: 'demo', user_id: 'demo' }); + const testFn = await engine.process({ client_id: 'demo' as ClientsId, user_id: 'demo' as UsersId }); expect(testFn).toBe(mockResponse); }) @@ -107,7 +108,7 @@ describe('Unit-tests (middleware), priority: class RateLimitsEngine', () => { const engine = new RateLimitsEngine([rule], count); const mockResponse = response; - const testFn = await engine.process({ client_id: 'demo', user_id: 'demo' }); + const testFn = await engine.process({ client_id: 'demo' as ClientsId, user_id: 'demo' as UsersId }); expect(testFn).toBe(mockResponse); }) diff --git a/backend/tests/unit-tests/middleware/maintenance.middleware.test.ts b/backend/tests/unit-tests/middleware/maintenance.middleware.test.ts index 29e2a5b..33cdb21 100644 --- a/backend/tests/unit-tests/middleware/maintenance.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/maintenance.middleware.test.ts @@ -6,9 +6,12 @@ import { MaintenanceMode } from '../../../src/utils/enums/maintenance-mode.enum' import { ErrorStatusCodes } from '../../../src/utils/errorStatusCodes.utils'; import { maintain, updateTrafficError } from '../../../src/middleware/maintenance.middleware'; import { default as mockId } from "../../mock-data/id.mock-data.json"; +import { MetaId } from '../../../src/repositories/interfaces/meta.entity.interface'; describe('Unit-tests (middleware), priority: synonym MaintenanceMiddleware', () => { + const mockValidMetaId = mockId.meta.valid[0] as MetaId; + describe('Middleware tests, priority: fn maintain()', () => { const mockTimestamp = '2025-01-01T14:00:00.000Z'; @@ -18,7 +21,7 @@ describe('Unit-tests (middleware), priority: synonym MaintenanceMiddleware', () let mockMaintenanceResponse: MaintenanceResponseDTO | null; beforeEach(() => { mockMaintenanceResponse = { - id: mockId.meta.valid[0], + id: mockValidMetaId, app: 'support', build_on: mockTimestamp, maintenance_mode: MaintenanceMode.A000, @@ -129,7 +132,7 @@ describe('Unit-tests (middleware), priority: synonym MaintenanceMiddleware', () let mockMaintenanceResponse: MaintenanceResponseDTO; beforeEach(() => { mockMaintenanceResponse = { - id: 1, + id: mockValidMetaId, app: 'support', build_on: mockTimestamp, maintenance_mode: MaintenanceMode.T011, diff --git a/backend/tests/unit-tests/middleware/rules/rate-limits.rules.middleware.test.ts b/backend/tests/unit-tests/middleware/rules/rate-limits.rules.middleware.test.ts index a41a50f..3c1b42b 100644 --- a/backend/tests/unit-tests/middleware/rules/rate-limits.rules.middleware.test.ts +++ b/backend/tests/unit-tests/middleware/rules/rate-limits.rules.middleware.test.ts @@ -9,6 +9,7 @@ import { import { TicketsResponseDTO } from "../../../../src/dtos/tickets.dto"; import { RateLimitsData, RateLimitsResponse } from "../../../../src/middleware/interfaces/rate-limits.interface.middleware"; import * as CommonUtils from "../../../../src/utils/common.utils"; +import { default as mockId } from "../../../mock-data/id.mock-data.json"; import ticketsService from "../../../../src/services/tickets.service"; import { TicketStatus } from "../../../../src/utils/enums/ticket-status.enum"; import rateLimitsService from "../../../../src/services/rate-limits.service"; @@ -16,41 +17,48 @@ import demoLimitsService from "../../../../src/services/demo-limits.service"; import { Violation } from "../../../../src/utils/enums/violations.enum"; import { MaintenanceMode } from "../../../../src/utils/enums/maintenance-mode.enum"; import { TicketOption } from "../../../../src/utils/enums/ticket-option.enum"; +import { ClientsId } from "../../../../src/repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../../../../src/repositories/interfaces/users.entity.interface"; +import { TicketsId } from "../../../../src/repositories/interfaces/tickets.entity.interface"; // Ensure correct type by converting secret to number via unary + operator. import { secrets } from "../../../../src/utils/secrets.utils" +import { MetaId } from "../../../../src/repositories/interfaces/meta.entity.interface"; describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () => { - const mockValidClientsId = 'valid_clients_test_id'; - const mockValidUsersId = 'valid_users_test_id'; + const mockValidTicketId = mockId.tickets.valid[0] as TicketsId; + const mockValidClientId = mockId.clients.valid[0] as ClientsId; + const mockValidUserId = mockId.users.valid[0] as UsersId; let mockRetryAfter: string; let mockParam_data: RateLimitsData; let mockBurstContext: TicketsResponseDTO[]; beforeEach(() => { mockRetryAfter = '2025-01-02T00:00:01.000Z'; mockParam_data = { - client_id: mockValidClientsId, - user_id: mockValidUsersId + client_id: mockValidClientId, + user_id: mockValidUserId }; mockBurstContext = [ { - ticket_id: 'valid_tickets_test_id_0', - client_id: mockValidClientsId, - user_id: mockValidUsersId, + ticket_id: mockValidTicketId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, + title: 'test_title_0', message: 'test_message_0', flag: null, last_modified: '2025-01-01T14:00:04.000Z', created_on: '2025-01-01T14:00:04.000Z' }, { - ticket_id: 'valid_tickets_test_id_1', - client_id: mockValidClientsId, - user_id: mockValidUsersId, + ticket_id: 'valid_tickets_test_id_1' as TicketsId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, + title: 'test_title_1', message: 'test_message_1', flag: null, last_modified: '2025-01-01T14:00:11.000Z', @@ -81,11 +89,12 @@ describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () const ruleCBL = new ClientsBurstLimitRule(3); const mockContext = structuredClone(mockBurstContext); mockContext.push({ - ticket_id: 'valid_tickets_test_id_2', - client_id: mockValidClientsId, - user_id: mockValidUsersId, + ticket_id: 'valid_tickets_test_id_2' as TicketsId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, + title: 'test_title_2', message: 'test_message_2', flag: null, last_modified: '2025-01-01T14:00:23.000Z', @@ -128,11 +137,12 @@ describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () const ruleUBL = new UsersBurstLimitRule(3); const mockContext = structuredClone(mockBurstContext); mockContext.push({ - ticket_id: 'valid_tickets_test_id_2', - client_id: mockValidClientsId, - user_id: mockValidUsersId, + ticket_id: 'valid_tickets_test_id_2' as TicketsId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, + title: 'test_title_2', message: 'test_message_2', flag: null, last_modified: '2025-01-01T14:00:23.000Z', @@ -196,7 +206,7 @@ describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () retryAfter: mockRetryAfter, penalty: { type: Violation.CLIENTSFLAG, - id: 'valid_clients_test_id', + id: mockValidClientId, penaltyValue: null } }; @@ -250,7 +260,7 @@ describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () retryAfter: mockRetryAfter, penalty: { type: Violation.USERSFLAG, - id: 'valid_users_test_id', + id: mockValidUserId, penaltyValue: null } }; @@ -302,7 +312,7 @@ describe('Unit-tests (middleware), priority: implementation RateLimitsRule', () retryAfter: mockRetryAfter, penalty: { type: Violation.MAINTENANCE_TRAFFIC, - id: 1, + id: mockId.meta.valid[0] as MetaId, penaltyValue: MaintenanceMode.T011 } }; diff --git a/backend/tests/unit-tests/models/clients.models.test.ts b/backend/tests/unit-tests/models/clients.models.test.ts index ecd6f19..0abe852 100644 --- a/backend/tests/unit-tests/models/clients.models.test.ts +++ b/backend/tests/unit-tests/models/clients.models.test.ts @@ -5,13 +5,14 @@ import { import * as CommonUtils from "../../../src/utils/common.utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; import clientsModel from "../../../src/models/clients.model"; -import { Clients } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import { ApiKeyStatus } from "../../../src/utils/enums/api-key-status.enum"; -const mockVar_apiKey = clientsModel._generateApiKeyObj(); +const mockValidClientId = mockId.clients.valid[0] as ClientsId; +const mockVar_apiKey = (clientsModel as any).generateApiKeyObj(); const mockTimestamp = '2025-01-01T14:00:02.000Z'; let mockData: Clients = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, name: 'testclient', api_key_hash: mockVar_apiKey.keyHash, status: ApiKeyStatus.ACTIVE, @@ -23,14 +24,14 @@ let mockData: Clients = { describe('Unit-tests (model), priority: entity Clients', () => { - describe('Priority: fn mapToCreateResponseDTO()', () => { + describe('Priority: fn toClientsCreateResponseDTO()', () => { describe('Testing valid fn calls', () => { test('Map timestamps of clients object, result: dto ClientsCreateResponseDTO', () => { const mockParam_data: Clients = structuredClone(mockData); - const testFn = clientsModel.mapToCreateResponseDTO(mockParam_data, mockVar_apiKey.keyRaw); + const testFn = clientsModel.toClientsCreateResponseDTO(mockParam_data, mockVar_apiKey.keyRaw); const expectResult: ClientsCreateResponseDTO = { client_id: mockParam_data.client_id, name: mockParam_data.name, @@ -52,10 +53,10 @@ describe('Unit-tests (model), priority: entity Clients', () => { describe('Testing valid fn calls', () => { test('Generate new object Clients + raw key', () => { + const mockParam_id = mockValidClientId; const mockParam_dto: ClientsCreateDTO = { name: 'TESTCLIENT' }; - const mockParam_id = mockId.clients.valid[0]; const mockApiKeyObj = { keyRaw: 'test-key', keyHash: 'hashed-test-key' }; const mockClient: Clients = { client_id: mockParam_id, @@ -69,7 +70,7 @@ describe('Unit-tests (model), priority: entity Clients', () => { }; jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(mockParam_id); - jest.spyOn(clientsModel, "_generateApiKeyObj").mockReturnValue(mockApiKeyObj); + jest.spyOn(clientsModel as any, "generateApiKeyObj").mockReturnValue(mockApiKeyObj); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp); const testFn = clientsModel.generateClientsCreateObj(mockParam_dto); diff --git a/backend/tests/unit-tests/models/feedback-rating.model.test.ts b/backend/tests/unit-tests/models/feedback-rating.model.test.ts index 7d205a6..6b7e997 100644 --- a/backend/tests/unit-tests/models/feedback-rating.model.test.ts +++ b/backend/tests/unit-tests/models/feedback-rating.model.test.ts @@ -1,9 +1,11 @@ import { FeedbackRatingCreateDTO, FeedbackRatingExtendedResponseDTO, FeedbackRatingResponseDTO, FeedbackRatingUpdateDTO } from "../../../src/dtos/feedback-rating.dto"; import feedbackRatingModel from "../../../src/models/feedback-rating.model"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import { FeedbackRating } from "../../../src/repositories/interfaces/feedback-rating.entity.interface"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as mockId from "../../mock-data/id.mock-data.json"; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; const mockTimestamp = '2025-01-01T14:00:09.000Z'; describe('Unit-tests (model), priority: entity FeedbackRating', () => { @@ -14,7 +16,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Generate new object, params: are undefined', () => { const mockParam_dto: FeedbackRatingCreateDTO = { - client_id: mockId.clients.valid[0] + client_id: mockValidClientId }; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp); @@ -32,7 +34,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Generate new object, params: have values', () => { const mockParam_dto: FeedbackRatingCreateDTO = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 1, rating_sum: 5 }; @@ -78,7 +80,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Convert entity to dto + map average rating, result: FeedbackRatingExtendedResponseDTO', () => { const mockParam_entity: FeedbackRating = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 16, rating_sum: 67, last_modified: mockTimestamp, @@ -97,7 +99,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Convert entity to dto + map average rating, result: FeedbackRatingResponseDTO', () => { const mockParam_entity: FeedbackRating = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 16, rating_sum: 67, last_modified: mockTimestamp, @@ -121,7 +123,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Convert entity[] to dto[] + map average rating, result: FeedbackRatingExtendedResponseDTO[]', () => { const mockParam_entities: FeedbackRating[] = [{ - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 16, rating_sum: 67, last_modified: mockTimestamp, @@ -141,7 +143,7 @@ describe('Unit-tests (model), priority: entity FeedbackRating', () => { test('Convert entity[] to dto[] + map average rating, result: FeedbackRatingResponseDTO[]', () => { const mockParam_entities: FeedbackRating[] = [{ - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 16, rating_sum: 67, last_modified: mockTimestamp, diff --git a/backend/tests/unit-tests/models/feedback.model.test.ts b/backend/tests/unit-tests/models/feedback.model.test.ts index f5b3d5e..34d1093 100644 --- a/backend/tests/unit-tests/models/feedback.model.test.ts +++ b/backend/tests/unit-tests/models/feedback.model.test.ts @@ -1,10 +1,15 @@ import { FeedbackRatingResponseDTO } from "../../../src/dtos/feedback-rating.dto"; import { FeedbackCreateDTO, FeedbackResponseDTO, FeedbackUpdateReviewDTO } from "../../../src/dtos/feedback.dto"; import feedbackModel from "../../../src/models/feedback.model"; -import { Feedback } from "../../../src/repositories/interfaces/feedback.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { Feedback, FeedbackId } from "../../../src/repositories/interfaces/feedback.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as mockId from "../../mock-data/id.mock-data.json"; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; +const mockValidUserId = mockId.users.valid[0] as UsersId; +const mockValidFeedbackId = mockId.feedback.valid[0] as FeedbackId; const mockTimestamp = '2025-01-01T14:00:08.000Z'; describe('Unit-tests (model), priority: entity Feedback', () => { @@ -12,9 +17,9 @@ describe('Unit-tests (model), priority: entity Feedback', () => { let mockEntity: Feedback; beforeEach(() => { mockEntity = { - feedback_id: mockId.feedback.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + feedback_id: mockValidFeedbackId, + client_id: mockValidClientId, + user_id: mockValidUserId, rating: 5, term_accepted: true, message: 'valid_feedback_test_message', @@ -121,9 +126,9 @@ describe('Unit-tests (model), priority: entity Feedback', () => { const originalEntity: Feedback = structuredClone(mockEntity); originalEntity.reviewed_on = mockTimestamp; const newEntity: Feedback = { - feedback_id: mockId.feedback.valid[1], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[1], + feedback_id: mockId.feedback.valid[1] as FeedbackId, + client_id: mockValidClientId, + user_id: mockId.users.valid[1] as UsersId, rating: 4, term_accepted: false, message: 'another_valid_feedback_test_message', diff --git a/backend/tests/unit-tests/models/rate-limits.models.test.ts b/backend/tests/unit-tests/models/rate-limits.models.test.ts index 691aaee..d44d8ca 100644 --- a/backend/tests/unit-tests/models/rate-limits.models.test.ts +++ b/backend/tests/unit-tests/models/rate-limits.models.test.ts @@ -1,13 +1,19 @@ import { RateLimitsCreateDTO } from "../../../src/dtos/rate-limits.dto"; import demoLimitsModel from "../../../src/models/demo-limits.model"; import rateLimitsModel from "../../../src/models/rate-limits.model"; -import { DemoLimits } from "../../../src/repositories/interfaces/demo-limits.entity.interface"; -import { RateLimits } from "../../../src/repositories/interfaces/rate-limits.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { DemoLimitsId, DemoLimits } from "../../../src/repositories/interfaces/demo-limits.entity.interface"; +import { RateLimitsId, RateLimits } from "../../../src/repositories/interfaces/rate-limits.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import * as CommonUtils from "../../../src/utils/common.utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; describe('Unit-tests (model), priority: entity RateLimits', () => { + const mockValidRateLimitId = mockId.rate_limits.valid[0] as RateLimitsId; + const mockValidClientId = mockId.clients.valid[0] as ClientsId; + const mockValidUserId = mockId.users.valid[0] as UsersId; + describe('Priority: fn mapCounts()', () => { describe('Testing valid fn calls', () => { @@ -15,17 +21,17 @@ describe('Unit-tests (model), priority: entity RateLimits', () => { test('Sum up count of entries, params: [].length === 2', () => { const mockParam_data: RateLimits[] = [ { - rate_limit_id: mockId.rate_limits.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + rate_limit_id: mockValidRateLimitId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', count: 3, last_modified: '2025-01-01T14:00:05.000Z' }, { - rate_limit_id: mockId.rate_limits.valid[1], - client_id: mockId.clients.valid[0], - user_id: 'another_valid_users_test_id', + rate_limit_id: mockId.rate_limits.valid[1] as RateLimitsId, + client_id: mockValidClientId, + user_id: mockId.users.valid[1] as UsersId, day: '2025-01-01', count: 2, last_modified: '2025-01-01T14:00:05.000Z' @@ -41,9 +47,9 @@ describe('Unit-tests (model), priority: entity RateLimits', () => { test('Sum up count of entries, params: [].length === 1', () => { const mockParam_data: RateLimits[] = [ { - rate_limit_id: mockId.rate_limits.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + rate_limit_id: mockValidRateLimitId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', count: 4, last_modified: '2025-01-01T14:00:05.000Z' @@ -73,8 +79,8 @@ describe('Unit-tests (model), priority: entity RateLimits', () => { test('Map timestamp/date values to DTO, result: entity Partial', () => { const mockParam_dto: RateLimitsCreateDTO = { - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + client_id: mockValidClientId, + user_id: mockValidUserId, } const mockDate = '2025-01-01'; const mockTimestamp = '2025-01-01T14:00:05.000Z'; @@ -106,7 +112,7 @@ describe('Unit-tests (model), priority: entity DemoLimits', () => { test('Sum up count of entries, params: [].length === 1', () => { const mockParam_data: DemoLimits[] = [ { - demo_limit_id: 1, + demo_limit_id: 1 as DemoLimitsId, day: '2025-01-01', count: 4, last_modified: '2025-01-01T14:00:05.000Z' diff --git a/backend/tests/unit-tests/models/tickets.model.test.ts b/backend/tests/unit-tests/models/tickets.model.test.ts index 3ab61f4..1e5cee7 100644 --- a/backend/tests/unit-tests/models/tickets.model.test.ts +++ b/backend/tests/unit-tests/models/tickets.model.test.ts @@ -2,18 +2,23 @@ import { TicketsCreateDTO, TicketsResponseDTO, TicketsUpdateDTO } from "../../.. import * as CommonUtils from "../../../src/utils/common.utils"; import * as mockId from "../../mock-data/id.mock-data.json"; import { TicketStatus } from "../../../src/utils/enums/ticket-status.enum"; -import { Tickets } from "../../../src/repositories/interfaces/tickets.entity.interface"; +import { Tickets, TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface"; import ticketsModel from "../../../src/models/tickets.model"; import { Readable } from 'stream'; import { FilesService } from "../../../src/services/files.service"; import { TicketOption } from "../../../src/utils/enums/ticket-option.enum"; import { PermissionException } from "../../../src/utils/exceptions/auth.exception"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +const mockValidTicketId = mockId.tickets.valid[0] as TicketsId; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; +const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-01T14:00:04.000Z'; describe('Unit-tests (model), priority: entity Tickets', () => { - describe('Priority: fn generateTicket()', () => { + describe('Priority: fn generateTicketEntity()', () => { let mockFile_pdf: Express.Multer.File; let mockFile_webp: Express.Multer.File; @@ -50,10 +55,10 @@ describe('Unit-tests (model), priority: entity Tickets', () => { describe('Testing valid fn calls', () => { test('Generate new object, priority: no files', async () => { - const mockParam_id = mockId.tickets.new[0]; + const mockParam_id = mockId.tickets.new[0] as TicketsId; const mockParam_dto: TicketsCreateDTO = { - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + client_id: mockValidClientId, + user_id: mockValidUserId, option: TicketOption.SUPPORT, title: 'test-title', message: 'test-message' @@ -63,7 +68,7 @@ describe('Unit-tests (model), priority: entity Tickets', () => { jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(mockParam_id); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp); - const testFn = await ticketsModel.generateTicket(mockParam_dto, mockParam_files); + const testFn = await ticketsModel.generateTicketEntity(mockParam_dto, mockParam_files); const expectResult: Tickets = { ticket_id: mockParam_id, client_id: mockParam_dto.client_id, @@ -81,10 +86,10 @@ describe('Unit-tests (model), priority: entity Tickets', () => { }) test('Generate new object, priority: single file', async () => { - const mockParam_id = mockId.tickets.new[0]; + const mockParam_id = mockId.tickets.new[0] as TicketsId; const mockParam_dto: TicketsCreateDTO = { - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + client_id: mockValidClientId, + user_id: mockValidUserId, option: TicketOption.SUPPORT, title: 'test-title', message: 'test-message' @@ -98,7 +103,7 @@ describe('Unit-tests (model), priority: entity Tickets', () => { jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths); - const testFn = await ticketsModel.generateTicket(mockParam_dto, mockParam_files); + const testFn = await ticketsModel.generateTicketEntity(mockParam_dto, mockParam_files); const expectResult: Tickets = { ticket_id: mockParam_id, client_id: mockParam_dto.client_id, @@ -117,10 +122,10 @@ describe('Unit-tests (model), priority: entity Tickets', () => { }) test('Generate new object, priority: multiple files', async () => { - const mockParam_id = mockId.tickets.new[0]; + const mockParam_id = mockId.tickets.new[0] as TicketsId; const mockParam_dto: TicketsCreateDTO = { - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + client_id: mockValidClientId, + user_id: mockValidUserId, option: TicketOption.SUPPORT, title: 'test-title', message: 'test-message' @@ -137,7 +142,7 @@ describe('Unit-tests (model), priority: entity Tickets', () => { jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths); - const testFn = await ticketsModel.generateTicket(mockParam_dto, mockParam_files); + const testFn = await ticketsModel.generateTicketEntity(mockParam_dto, mockParam_files); const expectResult: Tickets = { ticket_id: mockParam_id, client_id: mockParam_dto.client_id, @@ -190,9 +195,9 @@ describe('Unit-tests (model), priority: entity Tickets', () => { test('Check for file deletion => call FilesService.deleteFiles(), params: ', async () => { const mockParam_dto: TicketsResponseDTO = { - ticket_id: mockId.tickets.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + ticket_id: mockValidTicketId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -214,9 +219,9 @@ describe('Unit-tests (model), priority: entity Tickets', () => { test('Check for file deletion => does NOT call FilesService.deleteFiles(), params: ', async () => { const mockParam_dto: TicketsResponseDTO = { - ticket_id: mockId.tickets.valid[1], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title-without-resource_paths', @@ -239,9 +244,9 @@ describe('Unit-tests (model), priority: entity Tickets', () => { let mockParam_dto: TicketsResponseDTO; beforeEach(() => { mockParam_dto = { - ticket_id: mockId.tickets.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + ticket_id: mockValidTicketId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.CLOSED, option: TicketOption.SUPPORT, title: 'test-title', diff --git a/backend/tests/unit-tests/models/users.models.test.ts b/backend/tests/unit-tests/models/users.models.test.ts index c40b5da..4476869 100644 --- a/backend/tests/unit-tests/models/users.models.test.ts +++ b/backend/tests/unit-tests/models/users.models.test.ts @@ -1,20 +1,21 @@ -import { Users } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import { UsersCreateDTO } from "../../../src/dtos/users.dto"; import usersModel from "../../../src/models/users.model"; import * as CommonUtils from "../../../src/utils/common.utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; import { UserStatus } from "../../../src/utils/enums/user-status.enum"; +const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-03T14:00:03.000Z'; describe('Unit-tests (model), priority: entity Users', () => { - describe('Priority: fn generateUser()', () => { + describe('Priority: fn generateUserEntity()', () => { describe('Testing valid fn calls', () => { test('Generate new user object, params: valid ', () => { - const mockParam_id = mockId.users.valid[0]; + const mockParam_id = mockValidUserId; const mockParam_dto: UsersCreateDTO = { email: 'valid.user@test.com' }; @@ -22,9 +23,9 @@ describe('Unit-tests (model), priority: entity Users', () => { jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(mockParam_id); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp); - const testFn = usersModel.generateUser(mockParam_dto); + const testFn = usersModel.generateUserEntity(mockParam_dto); const expectResult: Users = { - user_id: mockParam_id, + user_id: mockValidUserId, email: mockParam_dto.email, status: UserStatus.ACTIVE, flag: null, diff --git a/backend/tests/unit-tests/repositories/clients.repositories.test.ts b/backend/tests/unit-tests/repositories/clients.repositories.test.ts index 41d4925..229e8ef 100644 --- a/backend/tests/unit-tests/repositories/clients.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/clients.repositories.test.ts @@ -10,7 +10,7 @@ import { DBConnection } from "../../../src/configs/db"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as MockUtils from "../../common.test-utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; -import { Clients } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { Clients, ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import { ApiKeyStatus } from "../../../src/utils/enums/api-key-status.enum"; import clientsRepository from "../../../src/repositories/clients.repository"; import clientsModel from "../../../src/models/clients.model"; @@ -26,11 +26,11 @@ jest.mock("../../../src/configs/db", () => { } }); -const mockVar_id = '9e024539-32e8-4317-8007-84a3956e6b57'; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; const mockVar_keyHash = secrets.TEST_APIKEY_HASH; const mockTimestamp = '2025-01-01T14:00:02.000Z'; const mockData: Clients = { - client_id: mockVar_id, + client_id: mockValidClientId, name: 'valid_clients_test_name', api_key_hash: mockVar_keyHash, status: ApiKeyStatus.ACTIVE, @@ -56,18 +56,18 @@ describe('Unit-tests (repository), priority: entity Clients', () => { test('Return data for existing entry, params: valid ', async () => { const mockResult: Clients | null = structuredClone(mockData); const mockClient = MockUtils.mapMockDbClient(mockResult); - const testFn = await clientsRepository.findById(mockVar_id); + const testFn = await clientsRepository.findById(mockValidClientId); expect(testFn).toEqual(mockResult); expect(DBConnection.getInstance).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining(sql), - expect.arrayContaining([mockVar_id]) + expect.arrayContaining([mockValidClientId]) ); }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockResult: Clients | null = null; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await clientsRepository.findById(mockParam_id); @@ -84,7 +84,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult: Clients | null = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -161,7 +161,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { test('Return data for existing entry, params: ', async () => { const mockParam_name = 'existing_clients_test_name'; const mockResult: ClientsStatusResponseDTO = { - client_id: 'valid_clients_test_id', + client_id: mockValidClientId, name: mockParam_name, status: ApiKeyStatus.ACTIVE, last_use: mockTimestamp, @@ -217,9 +217,9 @@ describe('Unit-tests (repository), priority: entity Clients', () => { let mockParam_entity: Clients; beforeEach(() => { sql = `INSERT`; - mockVar_apiKey = clientsModel._generateApiKeyObj(); + mockVar_apiKey = (clientsModel as any).generateApiKeyObj(); mockParam_entity = { - client_id: mockVar_id, + client_id: mockValidClientId, name: 'valid_clients_test_name', api_key_hash: mockVar_apiKey.keyHash, status: ApiKeyStatus.ACTIVE, @@ -275,7 +275,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.clients.valid[0]; + const mockParam_id = mockValidClientId; const mockValues = [mockParam_dto.status, mockTimestamp, mockParam_id]; const mockResult: ClientsStatusResponseDTO = { @@ -298,7 +298,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { }) test('Return null for non-existing entry by invalid id', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockValues = [mockParam_dto.status, mockTimestamp, mockParam_id]; const mockResult = null; @@ -317,7 +317,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -341,7 +341,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.clients.valid[0]; + const mockParam_id = mockValidClientId; const mockValues = [mockParam_dto.flag, mockTimestamp, mockParam_id]; const mockResult: ClientsFlagResponseDTO | null = { @@ -363,7 +363,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockValues = [mockParam_dto.flag, mockTimestamp, mockParam_id]; const mockResult: ClientsFlagResponseDTO | null = null; @@ -382,7 +382,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult: ClientsFlagResponseDTO | null = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -408,7 +408,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.clients.valid[0]; + const mockParam_id = mockValidClientId; const mockValues = [mockTimestamp, mockParam_id]; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(mockTimestamp); @@ -432,7 +432,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockValues = [mockTimestamp, mockParam_id]; const mockResult = null; @@ -451,7 +451,7 @@ describe('Unit-tests (repository), priority: entity Clients', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); diff --git a/backend/tests/unit-tests/repositories/db.repositories.test.ts b/backend/tests/unit-tests/repositories/db.repositories.test.ts index cb582da..a9d667f 100644 --- a/backend/tests/unit-tests/repositories/db.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/db.repositories.test.ts @@ -42,17 +42,17 @@ describe('Unit-tests (repository), class DBConnection', () => { describe('Testing valid fn calls', () => { - test('Fn _getConnectionString(), environment: development', () => { + test('Fn getConnectionString(), environment: development', () => { const mockParam_env = 'development'; - const testFn = mockDb._getConnectionString(mockParam_env); + const testFn = mockDb.getConnectionString(mockParam_env); const expectUser = 'postgres'; expect(testFn).toContain(expectUser); }) - test('Fn _getConnectionString(), environment: test', () => { + test('Fn getConnectionString(), environment: test', () => { const mockParam_env = 'test'; - const testFn = mockDb._getConnectionString(mockParam_env); + const testFn = mockDb.getConnectionString(mockParam_env); const expectUser = 'testuser'; expect(testFn).toContain(expectUser); diff --git a/backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts b/backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts index 8e1fea1..182f062 100644 --- a/backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts @@ -6,6 +6,7 @@ import { default as mockId } from "../../mock-data/id.mock-data.json"; import feedbackRatingRepository from "../../../src/repositories/feedback-rating.repository"; import { DBConnection } from "../../../src/configs/db"; import { FeedbackRatingUpdateDTO } from "../../../src/dtos/feedback-rating.dto"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; jest.mock("../../../src/configs/db", () => { return { @@ -15,9 +16,10 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidClientId = mockId.clients.valid[0] as ClientsId; const mockTimestamp = '2025-01-01T14:00:09.000Z'; const mockData: FeedbackRating = { - client_id: mockId.clients.valid[0], + client_id: mockValidClientId, count: 16, rating_sum: 67, last_modified: mockTimestamp, @@ -38,7 +40,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { }); test('Return data for existing entry, params: valid ', async () => { - const mockParam_id = mockId.clients.valid[0]; + const mockParam_id = mockValidClientId; const mockResult: FeedbackRating = structuredClone(mockData); const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await feedbackRatingRepository.findById(mockParam_id); @@ -52,7 +54,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockResult = null; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await feedbackRatingRepository.findById(mockParam_id); @@ -69,7 +71,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -146,7 +148,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { test('Return data for multiple existing entries', async () => { const mockData_entry0: FeedbackRating = structuredClone(mockData); const mockData_entry1: FeedbackRating = { - client_id: 'another_clients_test_id', + client_id: mockId.clients.valid[1] as ClientsId, count: 5, rating_sum: 19, last_modified: mockTimestamp, @@ -201,7 +203,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { beforeEach(() => { sql = `INSERT`; mockParam_entity = { - client_id: mockId.clients.new[0], + client_id: mockValidClientId, count: 0, rating_sum: 0, last_modified: mockTimestamp, @@ -265,7 +267,7 @@ describe('Unit-tests (repository), priority: entity FeedbackRating', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.clients.invalid[0]; + const mockParam_id = mockId.clients.invalid[0] as ClientsId; const mockValues: any[] = [mockParam_dto.rating]; const mockResult: FeedbackRating | null = null; diff --git a/backend/tests/unit-tests/repositories/feedback.repositories.test.ts b/backend/tests/unit-tests/repositories/feedback.repositories.test.ts index db760fd..712d5c2 100644 --- a/backend/tests/unit-tests/repositories/feedback.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/feedback.repositories.test.ts @@ -2,10 +2,12 @@ import * as MockUtils from "../../common.test-utils"; import * as CommonUtils from "../../../src/utils/common.utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; import { DBQueryErrorException } from "../../../src/utils/exceptions/db.exception"; -import { Feedback } from "../../../src/repositories/interfaces/feedback.entity.interface"; +import { Feedback, FeedbackId } from "../../../src/repositories/interfaces/feedback.entity.interface"; import { DBConnection } from "../../../src/configs/db"; import feedbackRepository from "../../../src/repositories/feedback.repository"; import { FeedbackFilterDTO, FeedbackUpdateReviewDTO } from "../../../src/dtos/feedback.dto"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; jest.mock("../../../src/configs/db", () => { return { @@ -15,11 +17,12 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidFeedbackId = mockId.feedback.valid[0] as FeedbackId; const mockTimestamp = '2025-01-01T14:00:08.000Z'; const mockData: Feedback = { - feedback_id: mockId.feedback.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + feedback_id: mockValidFeedbackId, + client_id: mockId.clients.valid[0] as ClientsId, + user_id: mockId.users.valid[0] as UsersId, rating: 5, term_accepted: true, message: 'valid_feedback_test_message', @@ -41,7 +44,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { }); test('Return data for existing entry, params: valid ', async () => { - const mockParam_id = mockId.feedback.valid[0]; + const mockParam_id = mockValidFeedbackId; const mockResult: Feedback = structuredClone(mockData); const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await feedbackRepository.findById(mockParam_id); @@ -55,7 +58,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_id = mockId.feedback.invalid[0]; + const mockParam_id = mockId.feedback.invalid[0] as FeedbackId; const mockResult = null; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await feedbackRepository.findById(mockParam_id); @@ -72,7 +75,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.feedback.invalid[0]; + const mockParam_id = mockId.feedback.invalid[0] as FeedbackId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -112,7 +115,9 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_dto = { user_id: [mockId.users.invalid[0], 'another_invalid_users_test_id'] }; + const mockParam_dto = { + user_id: [mockId.users.invalid[0], 'another_invalid_users_test_id'] as UsersId[] + }; const mockValues = mockParam_dto.user_id; const mockResult: Feedback[] | null = null; @@ -184,7 +189,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.feedback.valid[0]; + const mockParam_id = mockValidFeedbackId; const mockValues: any[] = [mockParam_dto.reviewed_on, mockParam_dto.last_modified]; const mockResult: Feedback | null = structuredClone(mockData); @@ -200,7 +205,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { }) test('Return data of changed entry, params: invalid ', async () => { - const mockParam_id = mockId.feedback.invalid[0]; + const mockParam_id = mockId.feedback.invalid[0] as FeedbackId; const mockValues: any[] = [mockParam_dto.reviewed_on, mockParam_dto.last_modified]; const mockResult: Feedback | null = null; @@ -219,7 +224,7 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { describe('Testing invalid fn calls', () => { test('', async () => { - const mockParam_id = mockId.feedback.invalid[0]; + const mockParam_id = mockId.feedback.invalid[0] as FeedbackId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); diff --git a/backend/tests/unit-tests/repositories/meta.repositories.test.ts b/backend/tests/unit-tests/repositories/meta.repositories.test.ts index 372045b..574400d 100644 --- a/backend/tests/unit-tests/repositories/meta.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/meta.repositories.test.ts @@ -1,6 +1,6 @@ import { DBConnection } from "../../../src/configs/db"; import metaRepository from "../../../src/repositories/meta.repository"; -import { Maintenance, Meta } from "../../../src/repositories/interfaces/meta.entity.interface"; +import { Maintenance, Meta, MetaId } from "../../../src/repositories/interfaces/meta.entity.interface"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as MockUtils from "../../common.test-utils"; import { MaintenanceMode } from "../../../src/utils/enums/maintenance-mode.enum"; @@ -17,9 +17,10 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidMetaId = mockId.meta.valid[0] as MetaId; const mockTimestamp = '2025-01-01T14:00:01.000Z'; const mockData: Meta = { - id: 1, + id: mockValidMetaId, app: "support", author: "yqni13", build_on: mockTimestamp, @@ -48,7 +49,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { }); test('Return data for existing entry, params: = 1', async () => { - const mockParam_id = mockId.meta.valid[0]; + const mockParam_id = mockValidMetaId; const mockResult: Meta = structuredClone(mockData); const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await metaRepository.findById(mockParam_id); @@ -62,7 +63,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { }) test('Return null for non-existing entry, params: = 0', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; const mockResult = null; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await metaRepository.findById(mockParam_id); @@ -79,7 +80,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -156,7 +157,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { test('Return data for multiple existing entries', async () => { const mockData_entry0 = structuredClone(mockData); const mockData_entry1 = structuredClone(mockData_entry0); - mockData_entry1['id'] = 2; + mockData_entry1['id'] = 2 as MetaId; mockData_entry1['app'] = 'valid_meta_test_name'; const mockResult: Meta[] = [mockData_entry0, mockData_entry1]; @@ -206,7 +207,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { }); test('Return data for existing entry, params: = "valid_meta_test_name"', async () => { - const mockParam_id = mockId.meta.valid[0];; + const mockParam_id = mockValidMetaId; const mockParam_name = 'valid_meta_test_name'; const mockResult: Maintenance = { id: mockParam_id, @@ -266,7 +267,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.meta.valid[0]; + const mockParam_id = mockValidMetaId; const mockValues: any[] = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); const mockResult: Meta = structuredClone(mockData); @@ -285,7 +286,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; const mockValues: any[] = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); @@ -307,7 +308,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -336,7 +337,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.meta.valid[0]; + const mockParam_id = mockValidMetaId; mockValues = [mockParam_dto.maintenance_mode, mockParam_dto.last_modified, mockParam_id]; const mockResult: Maintenance = { id: mockData.id, @@ -362,7 +363,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; mockValues = [mockParam_dto.maintenance_mode, mockParam_dto.last_modified, mockParam_id]; const mockResult = null; @@ -382,7 +383,7 @@ describe('Unit-tests (repository), priority: entity Meta', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.meta.invalid[0]; + const mockParam_id = mockId.meta.invalid[0] as MetaId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); diff --git a/backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts b/backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts index c4915dd..c738bb5 100644 --- a/backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts @@ -9,10 +9,12 @@ import { DBQueryErrorException } from "../../../src/utils/exceptions/db.exceptio import * as MockUtils from "../../common.test-utils"; import * as CommonUtils from "../../../src/utils/common.utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; -import { RateLimits } from "../../../src/repositories/interfaces/rate-limits.entity.interface"; -import { DemoLimits } from "../../../src/repositories/interfaces/demo-limits.entity.interface"; +import { RateLimitsId, RateLimits } from "../../../src/repositories/interfaces/rate-limits.entity.interface"; +import { DemoLimitsId, DemoLimits } from "../../../src/repositories/interfaces/demo-limits.entity.interface"; import { DemoLimitsCountDTO, DemoLimitsResponseDTO, DemoLimitsUpdateDTO } from "../../../src/dtos/demo-limits.dto"; import demoLimitsRepository from "../../../src/repositories/demo-limits.repository"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; jest.mock("../../../src/configs/db", () => { return { @@ -22,10 +24,11 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidRateLimitId = mockId.rate_limits.valid[0] as RateLimitsId; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; +const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-01T14:00:05.000Z'; const mockDate = '2025-01-01'; -const mockValidClientsId = mockId.clients.valid[0]; -const mockValidUsersId = mockId.users.valid[0]; const expectExceptionResult = DBQueryErrorException; const mockBoolean = false; @@ -38,7 +41,7 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { let mockParam_dto_countById: RateLimitsCountDTO; let mockParam_dto_countByDate: RateLimitsCountDTO; beforeEach(() => { - mockParam_dto_countById = { client_id: mockValidClientsId, day: '2025-01-01' }; + mockParam_dto_countById = { client_id: mockValidClientId, day: '2025-01-01' }; mockParam_dto_countByDate = { day: '2025-02-05' }; }) @@ -47,9 +50,9 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return entries by id and day, params: ', async () => { const mockResult: RateLimits[] = [ { - rate_limit_id: mockId.rate_limits.valid[0], - client_id: mockValidClientsId, - user_id: mockValidUsersId, + rate_limit_id: mockValidRateLimitId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', count: 3, last_modified: '2025-01-01T14:00:05.000Z' @@ -73,17 +76,17 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return entries by day, params: ', async () => { const mockResult: RateLimits[] = [ { - rate_limit_id: mockId.rate_limits.new[0], - client_id: mockValidClientsId, - user_id: mockValidUsersId, + rate_limit_id: mockId.rate_limits.new[0] as RateLimitsId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-02-05', count: 1, last_modified: '2025-02-05T04:17:05.000Z' }, { - rate_limit_id: mockId.rate_limits.new[1], - client_id: mockValidClientsId, - user_id: mockValidUsersId, + rate_limit_id: mockId.rate_limits.new[1] as RateLimitsId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-02-05', count: 1, last_modified: '2025-02-05T04:38:05.000Z' @@ -124,8 +127,8 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { let mockParam_entity: Partial; beforeEach(() => { mockParam_entity = { - client_id: mockValidClientsId, - user_id: mockValidUsersId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: mockDate, count: 1, last_modified: mockTimestamp @@ -136,9 +139,9 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return data for created entry, params: ', async () => { const mockResult: RateLimitsResponseDTO = { - rate_limit_id: mockId.rate_limits.valid[0], - client_id: mockValidClientsId, - user_id: mockValidUsersId, + rate_limit_id: mockValidRateLimitId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: mockDate, count: 1, last_modified: mockTimestamp @@ -179,8 +182,8 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { beforeEach(() => { sql = 'UPDATE'; mockParam_dto = { - client_id: mockValidClientsId, - user_id: mockValidUsersId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', last_modified: mockTimestamp }; @@ -188,7 +191,7 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return data of changed entry, params: valid ', async () => { const mockResult: RateLimitsResponseDTO | null = { - rate_limit_id: mockId.rate_limits.valid[0], + rate_limit_id:mockValidRateLimitId, client_id: mockParam_dto.client_id, user_id: mockParam_dto.user_id, day: '2025-01-01', @@ -209,8 +212,8 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return null for non-existing entry, params: invalid ', async () => { const mockParam_dto_null: RateLimitsUpdateDTO = { - client_id: mockValidClientsId, - user_id: mockValidUsersId, + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', last_modified: mockTimestamp }; @@ -233,8 +236,8 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Throw DBQueryErrorException by catch-block', async () => { const mockParam_dto: RateLimitsUpdateDTO = { - client_id: mockId.clients.invalid[0], - user_id: mockId.users.invalid[0], + client_id: mockValidClientId, + user_id: mockValidUserId, day: '2025-01-01', last_modified: mockTimestamp }; @@ -264,7 +267,7 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return entries by day, params: ', async () => { const mockResult: DemoLimits[] = [ { - demo_limit_id: 2, + demo_limit_id: 2 as DemoLimitsId, day: mockParam_dto.day, count: 1, last_modified: '2025-02-05T04:17:05.000Z' @@ -314,7 +317,7 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return data for created entry, params: >', async () => { const mockResult: DemoLimitsResponseDTO = { - demo_limit_id: 1, + demo_limit_id: 1 as DemoLimitsId, day: mockDate, count: 1, last_modified: mockTimestamp @@ -362,7 +365,7 @@ describe('Unit-tests (repository), priority: entity RateLimits', () => { test('Return data of changed entry, params: valid ', async () => { const mockResult: DemoLimitsResponseDTO | null = { - demo_limit_id: 1, + demo_limit_id: 1 as DemoLimitsId, day: '2025-01-01', count: 1, last_modified: mockTimestamp diff --git a/backend/tests/unit-tests/repositories/tickets.repositories.test.ts b/backend/tests/unit-tests/repositories/tickets.repositories.test.ts index e8465c8..7b7b90c 100644 --- a/backend/tests/unit-tests/repositories/tickets.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/tickets.repositories.test.ts @@ -2,13 +2,15 @@ import { DBConnection } from "../../../src/configs/db"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as MockUtils from "../../common.test-utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; -import { Tickets } from "../../../src/repositories/interfaces/tickets.entity.interface"; +import { Tickets, TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface"; import { DBQueryErrorException } from "../../../src/utils/exceptions/db.exception"; import { TicketStatus } from "../../../src/utils/enums/ticket-status.enum"; import ticketsRepository from "../../../src/repositories/tickets.repository"; import { TicketsFilterDTO, TicketsIntervalDTO, TicketsResponseDTO, TicketsResponseExtendedDTO, TicketsUpdateDTO } from "../../../src/dtos/tickets.dto"; import { TicketOption } from "../../../src/utils/enums/ticket-option.enum"; import { DeviceOption } from "../../../src/utils/enums/device-option.enum"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; jest.mock("../../../src/configs/db", () => { return { @@ -18,11 +20,14 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidTicketId = mockId.tickets.valid[0] as TicketsId; +const mockValidClientId = mockId.clients.valid[0] as ClientsId; +const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-01T14:00:04.000Z'; const mockData: Tickets = { - ticket_id: mockId.tickets.valid[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + ticket_id: mockValidTicketId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -67,7 +72,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockResult = null; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await ticketsRepository.findById(mockParam_id); @@ -84,7 +89,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -108,9 +113,9 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { test('Return data for multiple existing entries', async () => { const mockData_entry0: TicketsResponseDTO = structuredClone(mockData); const mockData_entry1: TicketsResponseDTO = { - ticket_id: 'another_valid_tickets_test_id', - client_id: 'another_valid_clients_test_id', - user_id: 'another_valid_users_test_id', + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: mockId.clients.valid[1] as ClientsId, + user_id: mockId.users.valid[1] as UsersId, status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, title: 'another-test-title', @@ -170,26 +175,22 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { let sql: string; let mockTimestamp_now: Date; - let mockClientId: string; - let mockUserId: string; beforeEach(() => { sql = `SELECT`; mockTimestamp_now = new Date('2025-01-01T14:01:50.000Z'); - mockClientId = mockId.clients.valid[0]; - mockUserId = mockId.users.valid[0]; }); test('Return data for existing entry, params: ', async () => { const mockParam_dto: TicketsIntervalDTO = { - client_id: mockClientId, + client_id: mockValidClientId, intervalTime: '1 minute' }; const mockResult: TicketsResponseDTO[] = [ mockData, { - ticket_id: 'another_valid_tickets_id', - client_id: mockClientId, - user_id: 'another_valid_users_id', + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: mockValidClientId, + user_id: mockId.users.valid[1] as UsersId, status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, title: 'valid-test-title', @@ -211,21 +212,21 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { expect(DBConnection.getInstance).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining(sql), - expect.arrayContaining([mockClientId, mockTimestamp_now, mockParam_dto.intervalTime]) + expect.arrayContaining([mockValidClientId, mockTimestamp_now, mockParam_dto.intervalTime]) ); }) test('Return data for existing entry, params: ', async () => { const mockParam_dto: TicketsIntervalDTO = { - user_id: mockUserId, + user_id: mockValidUserId, intervalTime: '1 minute' }; const mockResult: TicketsResponseDTO[] = [ mockData, { - ticket_id: 'another_valid_tickets_id', - client_id: 'another_valid_client_id', - user_id: mockUserId, + ticket_id: mockId.tickets.valid[1] as TicketsId, + client_id: mockId.clients.valid[1] as ClientsId, + user_id: mockValidUserId, status: TicketStatus.ACTIVE, option: TicketOption.SUPPORT, title: 'valid-test-title', @@ -247,13 +248,13 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { expect(DBConnection.getInstance).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining(sql), - expect.arrayContaining([mockUserId, mockTimestamp_now, mockParam_dto.intervalTime]) + expect.arrayContaining([mockValidUserId, mockTimestamp_now, mockParam_dto.intervalTime]) ); }) test('Return null for existing entry beyond time interval', async () => { const mockParam_dto: TicketsIntervalDTO = { - client_id: mockClientId, + client_id: mockValidClientId, intervalTime: '1 minute' }; const mockResult = null; @@ -266,7 +267,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { expect(DBConnection.getInstance).toHaveBeenCalled(); expect(mockClient.query).toHaveBeenCalledWith( expect.stringContaining(sql), - expect.arrayContaining([mockClientId, mockTimestamp_now, mockParam_dto.intervalTime]) + expect.arrayContaining([mockValidClientId, mockTimestamp_now, mockParam_dto.intervalTime]) ); }) }) @@ -275,7 +276,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { test('Throw DBQueryErrorException by catch-block', async () => { const mockParam_dto = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, intervalTime: '1 minute' }; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; @@ -317,7 +318,9 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_dto = { user_id: ['non-existing_users_test_id_0', 'non-existing_users_test_id_1'] }; + const mockParam_dto = { + user_id: [mockId.users.invalid[0], mockId.users.invalid[1]] as UsersId[] + }; const mockValues = mockParam_dto.user_id; const mockResult: Tickets[] | null = null; @@ -351,9 +354,9 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { describe('Database tests table , priority: fn create()', () => { const mockParam_entity: Tickets = { - ticket_id: mockId.tickets.new[0], - client_id: mockId.clients.valid[0], - user_id: mockId.users.valid[0], + ticket_id: mockId.tickets.new[0] as TicketsId, + client_id: mockValidClientId, + user_id: mockValidUserId, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'new-test-title', @@ -420,7 +423,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockData.user_id; + const mockParam_id = mockData.ticket_id; const mockValues: any[] = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); const mockResult: Tickets = structuredClone(mockData); @@ -438,7 +441,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { }) test('Return null for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockValues: any[] = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); @@ -458,7 +461,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -495,7 +498,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { }) test('Return false for non-existing entry, params: invalid ', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockResult = false; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await ticketsRepository.delete(mockParam_id); @@ -512,7 +515,7 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.tickets.invalid[0]; + const mockParam_id = mockId.tickets.invalid[0] as TicketsId; const mockErrorMsg = "DB ERROR ON DELETE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); diff --git a/backend/tests/unit-tests/repositories/users.repositories.test.ts b/backend/tests/unit-tests/repositories/users.repositories.test.ts index e14665a..e5b9385 100644 --- a/backend/tests/unit-tests/repositories/users.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/users.repositories.test.ts @@ -2,7 +2,7 @@ import { DBConnection } from "../../../src/configs/db"; import * as CommonUtils from "../../../src/utils/common.utils"; import * as MockUtils from "../../common.test-utils"; import { default as mockId } from "../../mock-data/id.mock-data.json"; -import { Users } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import { UserStatus } from "../../../src/utils/enums/user-status.enum"; import usersRepository from "../../../src/repositories/users.repository"; import { UsersUpdateDTO, UsersFlagUpdateDTO } from "../../../src/dtos/users.dto"; @@ -17,9 +17,10 @@ jest.mock("../../../src/configs/db", () => { } }); +const mockValidUserId = mockId.users.valid[0] as UsersId; const mockTimestamp = '2025-01-03T14:00:03.000Z'; const mockData: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'user@test.com', status: UserStatus.ACTIVE, flag: null, @@ -41,7 +42,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { }); test('Return data for existing entry, params: valid ', async () => { - const mockParam_id = mockId.users.valid[0]; + const mockParam_id = mockValidUserId; const mockResult: Users | null = structuredClone(mockData); const mockClient = MockUtils.mapMockDbClient(mockResult); @@ -56,7 +57,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { }) test('Return null for non-existing entry, params: non-existing ', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockResult: Users | null = null; const mockClient = MockUtils.mapMockDbClient(mockResult); @@ -74,7 +75,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockErrorMsg = "DB ERROR ON SELECT QUERY"; const mockResult: Users | null = null; @@ -152,7 +153,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { test('Return data for multiple existing entries', async () => { const mockData_entry0 = structuredClone(mockData); const mockData_entry1 = structuredClone(mockData_entry0); - mockData_entry1['user_id'] = 'another_valid_users_test_id'; + mockData_entry1['user_id'] = 'another_valid_users_test_id' as UsersId; mockData_entry1['email'] = 'user1@test.com'; const mockResult: Users[] = [mockData_entry0, mockData_entry1]; @@ -258,7 +259,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { beforeEach(() => { sql = 'INSERT'; mockParam_entity = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'new-user@test.com', status: UserStatus.ACTIVE, flag: null, @@ -315,7 +316,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.users.valid[0]; + const mockParam_id = mockValidUserId; const mockValues = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); const mockResult: Users = structuredClone(mockData); @@ -333,7 +334,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { }) test('Return null for no entries, params: non-existing ', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockValues = Object.values(mockParam_dto).map(value => value); mockValues.push(mockParam_id); @@ -354,7 +355,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); @@ -381,7 +382,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { describe('Testing valid fn calls', () => { test('Return data of changed entry, params: valid ', async () => { - const mockParam_id = mockId.users.valid[0]; + const mockParam_id = mockValidUserId; const mockValues = [mockParam_dto.flag, mockParam_dto.last_modified, mockParam_id]; const mockResult: Users = structuredClone(mockData); @@ -399,7 +400,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { }) test('Return null for no entries, params: non-existing ', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockValues = [mockParam_dto.flag, mockParam_dto.last_modified, mockParam_id]; const mockResult = null; @@ -419,7 +420,7 @@ describe('Unit-tests (repository), priority: entity Users', () => { describe('Testing invalid fn calls', () => { test('Throw DBQueryErrorException by catch-block', async () => { - const mockParam_id = mockId.users.invalid[0]; + const mockParam_id = mockId.users.invalid[0] as UsersId; const mockErrorMsg = "DB ERROR ON UPDATE QUERY"; const mockResult = null; jest.spyOn(CommonUtils, "logError").mockReturnValue(); diff --git a/backend/tests/unit-tests/utils/common.utils.test.ts b/backend/tests/unit-tests/utils/common.utils.test.ts index d8c197b..07a5b5e 100644 --- a/backend/tests/unit-tests/utils/common.utils.test.ts +++ b/backend/tests/unit-tests/utils/common.utils.test.ts @@ -1,4 +1,4 @@ -import { Users } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Users, UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; import * as CommonUtils from "../../../src/utils/common.utils"; import { Flag } from "../../../src/utils/enums/flag.enum"; import { TicketStatus } from "../../../src/utils/enums/ticket-status.enum"; @@ -9,6 +9,8 @@ import { default as mockId } from "../../mock-data/id.mock-data.json"; describe('Unit-tests (utils), priority: synonym CommonUtils', () => { + const mockValidUserId = mockId.users.valid[0] as UsersId; + describe('Testing valid fn calls', () => { test('Fn mapKeyToHash()', () => { @@ -54,7 +56,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { test('Fn mapObjTimestamps()', () => { const mockParam_data: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'user@test.com', status: UserStatus.ACTIVE, flag: null, @@ -65,7 +67,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { const testFn = CommonUtils.mapObjTimestamps(mockParam_data, mockParam_timeMapTargets); const expectResult: Users = { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'user@test.com', status: UserStatus.ACTIVE, flag: null, @@ -79,7 +81,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { test('Fn mapArrayTimestamps()', () => { const mockParam_data: Users[] = [ { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'user0@test.com', status: UserStatus.ACTIVE, flag: null, @@ -87,7 +89,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { created_on: '2025-01-01 14:00:03.000+01' }, { - user_id: mockId.users.new[0], + user_id: mockId.users.new[0] as UsersId, email: 'user1@test.com', status: UserStatus.ACTIVE, flag: null, @@ -100,7 +102,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { const testFn = CommonUtils.mapArrayTimestamps(mockParam_data, mockParam_timeMapTargets); const expectResult: Users[] = [ { - user_id: mockId.users.valid[0], + user_id: mockValidUserId, email: 'user0@test.com', status: UserStatus.ACTIVE, flag: null, @@ -108,7 +110,7 @@ describe('Unit-tests (utils), priority: synonym CommonUtils', () => { created_on: '2025-01-01T13:00:03.000Z' }, { - user_id: mockId.users.new[0], + user_id: mockId.users.new[0] as UsersId, email: 'user1@test.com', status: UserStatus.ACTIVE, flag: null, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a020ec8..4d6e52d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,9 +4,19 @@
+### $\textsf{\color{skyblue}2026/03/09}$ + +$\textsf{[v1.4.1\ =>\ {\textbf{\color{brown}v1.4.4}]}}$ app
+- $\textsf{\color{orange}Patch:}$ Updated: + + entity ID's are using now nominal types instead basic string|number. + + some model functions are renamed to keep consistency and improve readability. + + some api routes have been shortened to keep consistency and improve readability. + +
+ ### $\textsf{\color{skyblue}2026/03/07}$ -$\textsf{[v1.3.5\ =>\ {\textbf{\color{brown}v1.4.1}]}}$ app
+$\textsf{[v1.3.5\ =>\ v1.4.1]}$ app
$\textsf{[v1.5.4\ =>\ {\textbf{\color{brown}v1.6.0}]}}$ database - $\textsf{\color{teal}Addition:}$ Added api route + logic for Feedback/FeedbackRating to add new feedback/rating or get current average rating value. - $\textsf{\color{orange}Patch:}$ Updated: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..d63063c --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,12 @@ +## 🔑 $\textsf{\color{salmon}.env Setup}$ + +### Additional information to set `.env` up correctly : + +| Key | Information | +|-----|-------------| +| SECRET_ADMIN_KEY | free to create yourself | +| SECRET_DB_TEST_.... | use default data for all DB_TEST vars or [see setup](../backend/tests/db-migrations.setup.ts) | +| SECRET_TEST_APIKEY_RAW | use RAW from generateApiKeyObj() [see model](../backend/src/models/clients.model.ts) | +| SECRET_TEST_APIKEY_HASH | use HASH from same function as RAW | +| SECRET_EMAIL_PASS | password from SENDER email | + diff --git a/docs/DEVOPS.md b/docs/DEVOPS.md index 7bee7a7..1fe9c09 100644 --- a/docs/DEVOPS.md +++ b/docs/DEVOPS.md @@ -1,7 +1,4 @@ -## How to handle DevOps (Docker / Jenkins)? - -### `Docker` - +## 🐋 $\textsf{\color{salmon}How to run app in Docker container}$ Build image, volumes and container from docker-compose.yml of current path and start all (Whenever code-changes are registered, a new image will be built, otherwise the old last image will be restored.): diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 35492e9..096a567 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -94,3 +94,4 @@ Basic description: `...` [specific always sing |------------------------|-----------------------------|------------------------------| | S_ID | Service Identification | 'artdv', 'tava' | | Desc | Description | -- | +| Ta | Transaction | short form for fn naming | diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index cfb2c82..57cf22e 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,11 +1,11 @@ -## How to migrate? +## 🆕 $\textsf{\color{salmon}How to add new migration}$ -Create a new migration file (name scheme: -update.migrations).
This will be saved automatically at [migrations](../../migrations/). +Create a new migration file (ignore or follow name scheme: -update.migrations).
+This will be saved automatically at [migrations](../../migrations/). ```sh -npm run migrate create --.migrations +npx node-pg-migrate create --.migrations ``` - Adapt new migration file on `up` (target changes) and `down` (reset to current state). ```sh const shorthands = undefined; @@ -20,39 +20,34 @@ module.exports = { module.exports = { shorthands, down: (pgm) => { - // reset to current state + // reset to original state } } ``` -Since a central db is in use for this project, the migration can be executed locally. +
+ +## ▶️ $\textsf{\color{salmon}How to run migrations}$ + +### Since a central db is in use for this project, the migration can be executed locally. -Set connection string as environment variable by the following command in powershell
-local database: +
+ +Set connection string as environment variable in powershell for `local database`: ```sh path> $env:DATABASE_URL = "postgresql://:@:/" ``` -hosted database (like Neon): +... or for `hosted database` (like Neon): ```sh path> $env:DATABASE_URL = "postgresql://:@/?sslmode=require" ``` -(direction: 'up' or 'down') -for specific number of migrations (going from youngest to oldest): +To run migration (direction: 'up' or 'down') +specify number of migrations (going from youngest to oldest) or ignore `count` to run all: ```sh -path> npx node-pg-migrate #==> npx node-pg-migrate down 9999 -or -path> npm run migrate- -``` - -or, running all migrations including specified one (up/down, migration file `without .js`): - -```sh -path> $env:DATABASE_URL = -path> npx node-pg-migrate _ -``` - -
+path> npx node-pg-migrate -Check database on correct changes and reset type in `package.json`. \ No newline at end of file +#example (all migrations): npx node-pg-migrate up +#example (precise number): npx node-pg-migrate down 1
+``` \ No newline at end of file