diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c440b9..32ff23b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,9 +32,8 @@ jobs: env: NODE_ENV: ${{ secrets.NODE_ENV }} 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_NOTIFY_ADMIN_ID: ${{ secrets.SECRET_NOTIFY_ADMIN_ID }} + SECRET_NOTIFY_BOT_KEY: ${{ secrets.SECRET_NOTIFY_BOT_KEY }} 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 fc7d403..1548ceb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # yqni13 | $\texttt{\color{cornflowerblue}{SUPPORT}}$ -### $\textsf{\color{brown}{v1.4.9}}$ +### $\textsf{\color{brown}{v1.6.0}}$ #### 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 using API-Key authentication and rate-limiting. Created following Test-Driven Development (450+ tests) and hosting env:prod via Render, Neon and Cloudflare. @@ -16,6 +16,7 @@ Cloudflare Betterstack Testcontainers + Testcontainers

@@ -61,6 +62,7 @@ Alternatively run application in Docker container [(see docs)](./docs/DEVOPS.md) | ๐Ÿ” 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 | +| ๐Ÿ“ฌ Notifications | Admin/Developer notifications on certain events via Telegram bot |
@@ -96,20 +98,45 @@ In terms of rate-limiting, penalties and ready-to-extend functionality, the obse A certain set of rules checks for incoming requests on a total number for the day and within a certain time range. Before the engine returns found violations, the adapter calls for an increment of the daily rate-limit count. Violations are handled by the penalty handler (setting flags/status) and the workflow ends with either throwing an exception or calling next() to pass to the next middleware.
-  observe middleware diagram +  observe middleware diagram Figure 2 - observation middleware workflow, v1.0.0-beta.2
+
+ +### $\textsf{\color{teal}Notifications}$ + +While the support hub can receive requests for bugs, help, or feedback and create tickets based on this data, someone has to be informed of them if further action is necessary. Therefore, a notification service has been implemented to push messages to the responsible admin/developer.
+This notification service works with the `Telegram Bot API` by simply creating a bot and sending a custom message (text) with your credentials (`BOT_KEY`, `ADMIN_ID`) to the API: +```sh +private async notify(params: NotificationPostParams) { + try { + await axios.post(`https://api.telegram.org/bot${secrets.NOTIFY_BOT_KEY.trim()}/sendMessage`, { + chat_id: secrets.NOTIFY_ADMIN_ID.trim(), + text: params.text.trim(), + }); + } catch(err: any) { + CommonUtils.logError(params.logMsg, params.logMethod, err); + } +} +``` +Should the notification fail while the rest of the process was successfully executed, the error will only be logged and not thrown as an exception. Telegram can be used on mobile or desktop operating systems and displays notifications as standard messages with the customized text (see Figure 3, for example).
+[see Telegram Bot API](https://core.telegram.org/bots/tutorial) + +
+  Betterstack logging dashboard + Figure 3 - Notification via Telegram bot, v1.5.2 +

## ๐Ÿ“ $\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 3`). Additional meta data (environment + version numbers) help identifying and assigning errors. +
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 4`). Additional meta data (environment + version numbers) help identifying and assigning errors.
 Betterstack logging dashboard - Figure 3 - Betterstack logging dashboard, v1.0.0-beta.1 + Figure 4 - Betterstack logging dashboard, v1.0.0-beta.1

@@ -124,14 +151,14 @@ Testing of the application server can be done automatically via Jest tests (next [PAYLOAD] { "demo_mode": DemoMode } ``` Use `https://support-0hsq.onrender.com` for {{url}} to test on live conditions.
-See Figure 4 for the different use cases & responses (Postman, v11.73.5) - from left to right: +See Figure 5 for the different use cases & responses (Postman, v11.73.5) - from left to right:
[PAYLOAD]: { "mode_enum": "success" } => retrieve current version number as request without fail
[PAYLOAD]: undefined (none) or empty obj/array => retrieve exception for undefined body
[PAYLOAD]: { "mode_enum": "%ยง$" } => retrieve exception due to invalid value
[PAYLOAD]: { "mode_enum": "error" } => retrieve exception for intended failing db query (see data.message: SEL instead of SELECT)
 Betterstack logging dashboard - Figure 4 - /test/demo responses, v1.3.1 + Figure 5 - /test/demo responses, v1.3.1

@@ -170,16 +197,16 @@ or simply save as script command in `package.json` to run `npm test`:
To automatically check tests before merging feature/development branch further up, a `GitHub Action` is set up, see [main.yml](.github/workflows/main.yml).
-Preventing an unwanted merge with unfinished/failed test run, the project is set up to disable merging until all tests have passed (see Figure 5 to Figure 6). +Preventing an unwanted merge with unfinished/failed test run, the project is set up to disable merging until all tests have passed (see Figure 6 to Figure 7).
 GitHub processing tests - Figure 5 - processing tests, v0.9.1 + Figure 6 - processing tests, v0.9.1

 GitHub tests passed - Figure 6 - passing tests, v0.9.1 + Figure 7 - passing tests, v0.9.1

@@ -188,14 +215,8 @@ Preventing an unwanted merge with unfinished/failed test run, the project is set [see changelog for all updates](/docs/CHANGELOG.md) -$\textsf{[v1.4.4\ =>\ {\textbf{\color{brown}v1.4.9}]}}$ app
-- $\textsf{\color{teal}Addition:}$ Added form-data parser middleware for requests including files. -- $\textsf{\color{orange}Patch:}$ Updated: - + return types, mapping and handling (part 1). - + calculation for delete-permission by comparing timestamps (incorporate timezone offset on database read timestamp). - + 'tickets' request: more accurate check for containing files. - + use Promise-instance fn finally() in repository-layer to reduce code. - + documentation headers and display error on symbol (&). +$\textsf{[v1.5.2\ =>\ {\textbf{\color{brown}v1.6.0}]}}$ app
+- $\textsf{\color{teal}Addition:}$ Added health endpoints to improve health checks (manually + cron-jobs).
diff --git a/assets/diagram/db_migration_v1.5.2.png b/assets/diagram/db_migration_v1.5.2.png new file mode 100644 index 0000000..c791a8f Binary files /dev/null and b/assets/diagram/db_migration_v1.5.2.png differ diff --git a/assets/img/observe_middleware_diagram.png b/assets/diagram/observation_middleware_v1.5.2.png similarity index 100% rename from assets/img/observe_middleware_diagram.png rename to assets/diagram/observation_middleware_v1.5.2.png diff --git a/assets/diagram/public_workflow_v1.5.2.png b/assets/diagram/public_workflow_v1.5.2.png new file mode 100644 index 0000000..5f58588 Binary files /dev/null and b/assets/diagram/public_workflow_v1.5.2.png differ diff --git a/assets/icons/telegram.png b/assets/icons/telegram.png new file mode 100644 index 0000000..f5fad1f Binary files /dev/null and b/assets/icons/telegram.png differ diff --git a/assets/img/notification_example.jpg b/assets/img/notification_example.jpg new file mode 100644 index 0000000..7bd39f5 Binary files /dev/null and b/assets/img/notification_example.jpg differ diff --git a/backend/package-lock.json b/backend/package-lock.json index 36f4289..fec47d7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,6 +1,6 @@ { "name": "support_backend", - "version": "1.4.9", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/backend/package.json b/backend/package.json index 44b6337..55ca3ff 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "support_backend", - "version": "1.4.9", + "version": "1.6.0", "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 e21a57b..9d30e0e 100644 --- a/backend/src/configs/config.ts +++ b/backend/src/configs/config.ts @@ -2,9 +2,8 @@ export const Config: any = { 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, + NOTIFY_ADMIN_ID: process.env.SECRET_NOTIFY_ADMIN_ID || null, + NOTIFY_BOT_KEY: process.env.SECRET_NOTIFY_BOT_KEY || 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, diff --git a/backend/src/controllers/health.controller.ts b/backend/src/controllers/health.controller.ts new file mode 100644 index 0000000..880ec82 --- /dev/null +++ b/backend/src/controllers/health.controller.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from "express"; +import healthService from "../services/health.service"; +import { HealthCheckExtended } from "../services/interfaces/health.interface.service"; + +class HealthController { + async getHealthCheck(req: Request, res: Response, next: NextFunction) { + try { + res.status(200).json({ status: 200 }); + } catch(err: any) { + next(err); + } + } + + async getHealthCheckDetails(req: Request, res: Response, next: NextFunction) { + try { + const response: HealthCheckExtended = await healthService.getHealthCheckDetails(); + res.json(response); + } catch(err: any) { + next(err); + } + } +} + +export default new HealthController(); \ No newline at end of file diff --git a/backend/src/controllers/mailing.controller.ts b/backend/src/controllers/mailing.controller.ts deleted file mode 100644 index 3675315..0000000 --- a/backend/src/controllers/mailing.controller.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextFunction, Request, Response } from "express"; -import MailingService from '../services/mailing.service'; -const { checkValidation } = require('../middleware/validation.middleware'); - -class MailingController { - sendMail = async (req: Request, res: Response, next: NextFunction) => { - try { - checkValidation(req); - const response = await MailingService.sendMail(req.body, null); // TODO(yqni13): files param missing - res.json(response); - } catch(err: any) { - next(err); - } - } -} - -export default new MailingController(); \ No newline at end of file diff --git a/backend/src/dtos/feedback.dto.ts b/backend/src/dtos/feedback.dto.ts index 41eedf6..13a4029 100644 --- a/backend/src/dtos/feedback.dto.ts +++ b/backend/src/dtos/feedback.dto.ts @@ -57,3 +57,8 @@ export interface FeedbackResponseDTO { created_on: string, blocked?: boolean } + +export interface FeedbackExtendedResponseDTO extends FeedbackResponseDTO { + client_name: string, + user_email: string +} \ No newline at end of file diff --git a/backend/src/loaders/routes.loader.ts b/backend/src/loaders/routes.loader.ts index b051f72..8a47dfd 100644 --- a/backend/src/loaders/routes.loader.ts +++ b/backend/src/loaders/routes.loader.ts @@ -1,22 +1,22 @@ import { Application } from 'express'; import clientsRouter from '../routes/clients.route'; -import mailingRouter from '../routes/mailing.route'; import metaRouter from '../routes/meta.route'; import ticketsRouter from '../routes/tickets.route'; import usersRouter from '../routes/users.route'; import testRouter from '../routes/test.route'; import feedbackRouter from '../routes/feedback.route'; import feedbackRatingRouter from '../routes/feedback-rating.route'; +import healthRouter from '../routes/health.route'; export class RoutesLoader { static initRoutes(app: Application, version: string) { app.use(`/api/${version}/clients`, clientsRouter); app.use(`/api/${version}/feedback`, feedbackRouter); app.use(`/api/${version}/feedback-rating`, feedbackRatingRouter); - app.use(`/api/${version}/mailing`, mailingRouter); + app.use(`/api/${version}/health`, healthRouter); app.use(`/api/${version}/meta`, metaRouter); app.use(`/api/${version}/test`, testRouter); app.use(`/api/${version}/tickets`, ticketsRouter); app.use(`/api/${version}/users`, usersRouter); - } + }; } \ No newline at end of file diff --git a/backend/src/middleware/handler/penalty.handler.middleware.ts b/backend/src/middleware/handler/penalty.handler.middleware.ts index 73025b6..7005319 100644 --- a/backend/src/middleware/handler/penalty.handler.middleware.ts +++ b/backend/src/middleware/handler/penalty.handler.middleware.ts @@ -14,6 +14,7 @@ import usersService from "../../services/users.service"; import { UsersFlagUpdateDTO } from "../../dtos/users.dto"; import { MaintenanceUpdateDTO } from "../../dtos/meta.dto"; import metaService from "../../services/meta.service"; +import { NotificationService } from "../../services/notificiation.service"; export class PenaltyHandler{ constructor(private readonly handlers: Map) { @@ -35,8 +36,15 @@ export class ClientsFlagPenalty implements PenaltyApply `${Math.round(num / 1024 / 1024)}MB`; + return { + heapUsed: toMB(memory.heapUsed), + heapTotal: toMB(memory.heapTotal), + rss: toMB(memory.rss) + } + } +} + +export default new HealthModel(); \ No newline at end of file diff --git a/backend/src/models/mailing.model.js b/backend/src/models/mailing.model.js deleted file mode 100644 index d2edac6..0000000 --- a/backend/src/models/mailing.model.js +++ /dev/null @@ -1,97 +0,0 @@ -const Secrets = require('../utils/secrets.utils'); -const nodemailer = require('nodemailer'); -const { decryptRSA } = require('../utils/crypto.utils'); -const { UnexpectedException } = require('../utils/exceptions/common.exception'); -const { AuthenticationStandardException } = require('../utils/exceptions/auth.exception'); -const Utils = require('../utils/common.utils'); -const logger = require('../logger/config.logger').getLogger(); - -class MailingModel { - sendMail = async (params) => { - if(!Object.keys(params).length) { - return { error: 'no params found' }; - } - - // const key = Utils.selectPrivateKey(params['source']); - // const sourceID = Utils.getSourceID(params['source']); - - const sender = params['sender']; - // const sender = decryptRSA(params['sender'], key); - // const messageData = { - // criteria: params['subject'], - // message: params['body'], - // attachments: params['data'] - // } - const messageData = ''; - const message = this.configMessage(messageData); - - try { - const mailOptions = { - from: Secrets.EMAIL_SENDER, - to: Secrets.EMAIL_RECEIVER, - replyTo: sender, - subject: `Support-Ticket #`, - text: message, - attachments: [ - { - filename: 'test_img.jpg', - path: 'test_img.jpg' - } - ] - } - const success = await this.wrapedSendMail(mailOptions); - return { response: { success, sender }}; - } catch(err) { - logger.error("ERROR ON SENDMAIL", { - error: err.code, - stack: err.stack, - context: { - method: 'support_mailing_sendMail', - params - } - }); - if(err.status === 535) { - throw new AuthenticationStandardException('server-535-auth#email-service', { data: err.message}); - } else { - throw new UnexpectedException(); - } - } - } - - async wrapedSendMail(mailOptions) { - return new Promise((resolve, reject) => { - const transporter = nodemailer.createTransport({ - service: 'gmx', - host: 'mail.gmx.net', - port: 465, - secure: true, - tls: { - secure: true, - ciphers: 'SSLv3', - rejectUnauthorized: false - }, - auth: { - user: Secrets.EMAIL_SENDER, - pass: Secrets.EMAIL_PASS - } - }); - - transporter.sendMail(mailOptions, function(err, info) { - if(err) { - if(err.responseCode === 535) { - return reject(new AuthenticationStandardException(err)); - } else { - return reject(new UnexpectedException(err)); - } - } - resolve(true); - }) - }) - } - - configMessage(data) { - return 'demo'; - } -} - -module.exports = new MailingModel(); \ No newline at end of file diff --git a/backend/src/models/test.model.ts b/backend/src/models/test.model.ts index d8dfe50..ba6c2ba 100644 --- a/backend/src/models/test.model.ts +++ b/backend/src/models/test.model.ts @@ -2,7 +2,7 @@ import { TestErrorDTO } from "../dtos/test.dto"; 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 { InternalServerException, MaintenanceException } from "../utils/exceptions/common.exception"; import { DBConnectionException, DBConstraintErrorException, DBEmptyException, DBQueryErrorException } from "../utils/exceptions/db.exception"; import { InvalidFilesException, InvalidPropertiesException } from "../utils/exceptions/validation.exception"; @@ -31,10 +31,6 @@ class TestModel { // CommonException case('InternalServerException'): throw new InternalServerException(); - case('RequestExceedMaxException'): - throw new RequestExceedMaxException(); - case('InvalidSourceException'): - throw new InvalidSourceException(); case('MaintenanceException'): throw new MaintenanceException(dto.errorMsg as MaintenanceMode ?? MaintenanceMode.E013); // DBConnectionException diff --git a/backend/src/repositories/feedback.repository.ts b/backend/src/repositories/feedback.repository.ts index 9b84ba2..9b1edb5 100644 --- a/backend/src/repositories/feedback.repository.ts +++ b/backend/src/repositories/feedback.repository.ts @@ -1,6 +1,6 @@ import { PoolClient, QueryResult } from "pg"; import { DBConnection } from "../configs/db"; -import { FeedbackFilterDTO, FeedbackResponseDTO, FeedbackUpdateReviewDTO } from "../dtos/feedback.dto"; +import { FeedbackExtendedResponseDTO, FeedbackFilterDTO, FeedbackUpdateReviewDTO } from "../dtos/feedback.dto"; import { Feedback, FeedbackId } from "./interfaces/feedback.entity.interface"; import { DBQueryErrorException } from "../utils/exceptions/db.exception"; import { logError } from "../utils/common.utils"; @@ -90,14 +90,14 @@ class FeedbackRepository { * * @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). + * @returns {FeedbackExtendedResponseDTO | null} FeedbackExtendedResponseDTO expands `Feedback` by 'rating_old', joined 'client_name' + 'user_email', 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 { + async upsertInTa(client: PoolClient, entity: Partial): Promise { const sql = ` WITH pre_update_data AS ( SELECT rating FROM ${this.table} - WHERE client_id = $1 AND user_id = $2 + WHERE ${this.table}.client_id = $1 AND ${this.table}.user_id = $2 ), upsert AS ( INSERT INTO ${this.table} @@ -114,17 +114,26 @@ class FeedbackRepository { RETURNING ${this.table}.*, (SELECT rating FROM pre_update_data) AS rating_old, + (SELECT name FROM clients WHERE clients.client_id = ${this.table}.client_id) AS client_name, + (SELECT email FROM users WHERE users.user_id = ${this.table}.user_id) AS user_email, false AS blocked ) SELECT * FROM upsert UNION ALL - SELECT *, NULL AS rating_old, true AS blocked + SELECT + ${this.table}.*, + NULL AS rating_old, + clients.name AS client_name, + users.email AS user_email, + true AS blocked FROM ${this.table} - WHERE client_id = $1 AND user_id = $2 + LEFT JOIN clients ON ${this.table}.client_id = clients.client_id + LEFT JOIN users ON ${this.table}.user_id = users.user_id + WHERE ${this.table}.client_id = $1 AND ${this.table}.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); + const result: QueryResult = await client.query(sql, values); return result.rows[0] ?? null; } } diff --git a/backend/src/repositories/health.repository.ts b/backend/src/repositories/health.repository.ts new file mode 100644 index 0000000..6bc00fe --- /dev/null +++ b/backend/src/repositories/health.repository.ts @@ -0,0 +1,23 @@ +import { DBConnection } from "../configs/db"; + +class HealthRepository { + async checkDatabase(): Promise { + const sql = 'SELECT 1'; + const db = DBConnection.getInstance(); + let client: any; + try { + client = await db.connect(); + await client.query(sql); + return { status: 'ok' }; + } catch(err: any) { + return { + status: 'error', + message: err instanceof Error ? err.message : 'Unknown error' + } + } finally { + await db.close(client); + } + } +} + +export default new HealthRepository(); \ No newline at end of file diff --git a/backend/src/repositories/tickets.repository.ts b/backend/src/repositories/tickets.repository.ts index 522b38f..3f23c9e 100644 --- a/backend/src/repositories/tickets.repository.ts +++ b/backend/src/repositories/tickets.repository.ts @@ -116,17 +116,28 @@ IDeleteRepository } } - async create(entity: Tickets): Promise { - const sql = `INSERT INTO ${this.table} - (ticket_id, client_id, user_id, status, option, title, message, resource_paths, flag, info_browser, info_os, info_device, last_modified, created_on) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) - RETURNING *;`; + async create(entity: Tickets): Promise { + const sql = ` + WITH inserted AS( + INSERT INTO ${this.table} + (ticket_id, client_id, user_id, status, option, title, message, resource_paths, flag, info_browser, info_os, info_device, last_modified, created_on) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + RETURNING * + ) + SELECT + inserted.*, + clients.name AS client_name, + users.email AS user_email + FROM inserted + LEFT JOIN clients ON inserted.client_id = clients.client_id + LEFT JOIN users ON inserted.user_id = users.user_id; + `; const values = [entity.ticket_id, entity.client_id, entity.user_id, entity.status, entity.option, entity.title, entity.message, entity.resource_paths, entity.flag, entity.info_browser, entity.info_os, entity.info_device, entity.last_modified, entity.created_on]; const db = DBConnection.getInstance(); let client: any; try { client = await db.connect(); - const result: QueryResult = await client.query(sql, values); + const result: QueryResult = await client.query(sql, values); return result.rows[0]; } catch(err: any) { const message = "DB ERROR ON INSERT QUERY"; diff --git a/backend/src/routes/health.route.ts b/backend/src/routes/health.route.ts new file mode 100644 index 0000000..d152e6d --- /dev/null +++ b/backend/src/routes/health.route.ts @@ -0,0 +1,18 @@ +import { Router } from "express"; +import { awaitHandlerFactory as factory } from '../middleware/awaitHandlerFactory.middleware'; +import healthController from "../controllers/health.controller"; +import { authAdmin } from "../middleware/auth.admin.middleware"; + +const router = Router(); + +// public +router.get('/', factory(healthController.getHealthCheck)); + +// admin only +router.get( + '/details', + authAdmin(), + factory(healthController.getHealthCheckDetails) +) + +export default router; \ No newline at end of file diff --git a/backend/src/routes/mailing.route.ts b/backend/src/routes/mailing.route.ts deleted file mode 100644 index 03f8129..0000000 --- a/backend/src/routes/mailing.route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express'; -import { awaitHandlerFactory } from '../middleware/awaitHandlerFactory.middleware'; -import MailingController from '../controllers/mailing.controller'; -import { mailingSchema } from '../validation/schemata/mailing.schema.validation'; - -const router = Router(); - -router.post('/send', mailingSchema, awaitHandlerFactory(MailingController.sendMail)); - -export default router; \ No newline at end of file diff --git a/backend/src/services/feedback.service.ts b/backend/src/services/feedback.service.ts index b07e42d..e6cb927 100644 --- a/backend/src/services/feedback.service.ts +++ b/backend/src/services/feedback.service.ts @@ -1,6 +1,7 @@ import { FeedbackCreateDTO, FeedbackCreateResponseDTO, + FeedbackExtendedResponseDTO, FeedbackFilterDTO, FeedbackResponseDTO, FeedbackUpdateReviewDTO @@ -14,6 +15,7 @@ import feedbackRepository from "../repositories/feedback.repository"; import { Feedback, FeedbackId } from "../repositories/interfaces/feedback.entity.interface"; import feedbackRatingService from "./feedback-rating.service"; import { DBConstraintErrorException } from "../utils/exceptions/db.exception"; +import { NotificationService } from "./notificiation.service"; class FeedbackService { async getFeedbackById(id: FeedbackId): Promise { @@ -36,7 +38,7 @@ class FeedbackService { return RepoUtils.asTransaction(message, method, async(client) => { const entity: Partial = feedbackModel.generateFeedbackEntity(dto); - const result: FeedbackResponseDTO | null = await feedbackRepository.upsertInTa(client, entity); + const result: FeedbackExtendedResponseDTO | null = await feedbackRepository.upsertInTa(client, entity); let dtoUpdateFR: FeedbackRatingUpdateDTO; if(!result) { @@ -64,12 +66,28 @@ class FeedbackService { }; await feedbackRatingService.createFeedbackRatingInTa(client, dtoCreateFR); } + const rating_average = update?.rating_average ?? dto.rating; + const convertedCreatedOn = CommonUtils.getTimestampUTC(new Date(result.created_on)); + + if(dto.message) { + // Notify on new rating including criticism or praise. + const notification = NotificationService.getInstance(); + await notification.sendFeedbackInfo({ + feedback_id: result.feedback_id, + client_name: result.client_name, + user_email: result.user_email, + rating: result.rating, + rating_average: rating_average, + term_accepted: result.term_accepted, + created_on: convertedCreatedOn + }); + } return { rating: result.rating, rating_old: result.rating_old, - rating_average_new: update?.rating_average ?? dto.rating, - created_on: CommonUtils.getTimestampUTC(new Date(result.created_on)) + rating_average_new: rating_average, + created_on: convertedCreatedOn } }) } diff --git a/backend/src/services/health.service.ts b/backend/src/services/health.service.ts new file mode 100644 index 0000000..763baed --- /dev/null +++ b/backend/src/services/health.service.ts @@ -0,0 +1,20 @@ +import healthModel from "../models/health.model"; +import healthRepository from "../repositories/health.repository"; +import { EnvMode } from "../utils/enums/env-mode.enum"; +import { secrets } from "../utils/secrets.utils"; +import { HealthCheckExtended } from "./interfaces/health.interface.service"; + +class HealthService { + async getHealthCheckDetails(): Promise { + const statusDatabase = await healthRepository.checkDatabase(); + const statusMemory = healthModel.checkMemory(); + return { + status: statusDatabase && statusMemory ? 200 : 503, + environment: secrets.ENV_MODE.trim() as EnvMode, + db: statusDatabase, + memory: statusMemory, + }; + } +} + +export default new HealthService(); \ No newline at end of file diff --git a/backend/src/services/interfaces/health.interface.service.ts b/backend/src/services/interfaces/health.interface.service.ts new file mode 100644 index 0000000..6a83120 --- /dev/null +++ b/backend/src/services/interfaces/health.interface.service.ts @@ -0,0 +1,22 @@ +import { EnvMode } from "../../utils/enums/env-mode.enum" + +export interface HealthCheck { + status: number +} + +export interface HealthCheckExtended extends HealthCheck { + environment: EnvMode, + db: HealthCheckDatabase, + memory: HealthCheckMemory, +} + +export interface HealthCheckDatabase { + status: string, + message?: any +} + +export interface HealthCheckMemory { + heapUsed: string, + heapTotal: string, + rss: string +} \ No newline at end of file diff --git a/backend/src/services/interfaces/notification.interface.service.ts b/backend/src/services/interfaces/notification.interface.service.ts new file mode 100644 index 0000000..25ff969 --- /dev/null +++ b/backend/src/services/interfaces/notification.interface.service.ts @@ -0,0 +1,41 @@ +import { FeedbackId } from "../../repositories/interfaces/feedback.entity.interface"; +import { TicketsId } from "../../repositories/interfaces/tickets.entity.interface"; +import { TicketOption } from "../../utils/enums/ticket-option.enum"; +import { Violation } from "../../utils/enums/violations.enum"; + +export interface NotificationPostParams { + text: string, + logMsg: string, + logMethod: string +} + +export interface NotificationBaseParams { + client_name: string, + user_email: string, + created_on: string +} + +export interface NotificationTicketsParams extends NotificationBaseParams { + ticket_id: TicketsId, + option: TicketOption, + title: string, +} + +export interface NotificationFeedbackParams extends NotificationBaseParams { + feedback_id: FeedbackId, + rating: number, + rating_average: number, + term_accepted: boolean +} + +/** + * @description Due to less priority and structure id and entity are used only instead name and email. + */ +export interface NotificationPenaltyParams { + id: string | number, + entity: string, + client_name?: string, + user_email?: string, + violation: Violation, + penalty: any, +} \ No newline at end of file diff --git a/backend/src/services/mailing.service.ts b/backend/src/services/mailing.service.ts deleted file mode 100644 index 3c808fa..0000000 --- a/backend/src/services/mailing.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -// const MailingModel = require('../models/mailing.model'); - -class MailingService { - sendMail = async (params: any, files?: any) => { - const hasParams = Object.keys(params).length !== 0; - let mail = {}; - // let mail = await MailingModel.sendMail(hasParams ? params : {}); - return mail; - } -} - -export default new MailingService(); \ No newline at end of file diff --git a/backend/src/services/notificiation.service.ts b/backend/src/services/notificiation.service.ts new file mode 100644 index 0000000..6041244 --- /dev/null +++ b/backend/src/services/notificiation.service.ts @@ -0,0 +1,71 @@ +import { + NotificationFeedbackParams, + NotificationPenaltyParams, + NotificationPostParams, + NotificationTicketsParams +} from "./interfaces/notification.interface.service"; +import { secrets } from "../utils/secrets.utils"; +import { EnvMode } from "../utils/enums/env-mode.enum"; +import axios from "axios"; +import * as CommonUtils from "../utils/common.utils"; + +export class NotificationService { + private static instance: NotificationService; + + static getInstance(): NotificationService { + if(!NotificationService.instance) { + NotificationService.instance = new NotificationService(); + } + return NotificationService.instance; + } + + private async notify(params: NotificationPostParams) { + try { + await axios.post(`https://api.telegram.org/bot${secrets.NOTIFY_BOT_KEY.trim()}/sendMessage`, { + chat_id: secrets.NOTIFY_ADMIN_ID.trim(), + text: params.text.trim(), + }); + } catch(err: any) { + CommonUtils.logError(params.logMsg, params.logMethod, err); + } + } + + private generateTitle(name: string): string { + const env = secrets.ENV_MODE.trim(); + if(env === EnvMode.DEV) { + return `SUPPORT:${name.toUpperCase()}:TESTING-DEV`; + } else { + return `SUPPORT:${name.toUpperCase()}`; + } + } + + async sendTicketInfo(data: NotificationTicketsParams) { + const message = `${this.generateTitle('ticket')}\nID: ${data.ticket_id}\nCLIENT: ${data.client_name}\nUSER: ${data.user_email}\nOPTION: ${data.option}\nTITLE: ${data.title}\nDATE: ${CommonUtils.getTimestampUTC(new Date(data.created_on))}`; + + await this.notify({ + text: message, + logMsg: 'NOTIFICATION ERROR ON TICKETS NOTIFY', + logMethod: 'Support_NotificationService_sendTicketInfo' + }); + } + + async sendFeedbackInfo(data: NotificationFeedbackParams) { + const message = `${this.generateTitle('feedback')}\nID: ${data.feedback_id}\nCLIENT: ${data.client_name}\nUSER: ${data.user_email}\nRATING: ${data.rating}\nAVERAGE: ${data.rating_average}\nTERM: ${data.term_accepted}\nDATE: ${CommonUtils.getTimestampUTC(new Date(data.created_on))}`; + + await this.notify({ + text: message, + logMsg: 'NOTIFICATION ERROR ON FEEDBACK NOTIFY', + logMethod: 'Support_NotificationService_sendFeedbackInfo' + }); + } + + async sendPenaltyInfo(data: NotificationPenaltyParams) { + const message = `${this.generateTitle('penalty')}\nENTITY: ${data.entity}\nID: ${String(data.id)}\n${data.client_name ? 'NAME: ' + data.client_name + '\n' : ''}${data.user_email ? 'EMAIL: ' + data.user_email + '\n' : ''}VIOLATION: ${data.violation}\nPENALTY: ${data.penalty}\nDATE: ${CommonUtils.getTimestampUTC(new Date())}`; + + await this.notify({ + text: message, + logMsg: 'NOTIFICATION ERROR ON PENALTY NOTIFY', + logMethod: 'Support_NotificationService_sendPenaltyInfo' + }); + } +} \ No newline at end of file diff --git a/backend/src/services/tickets.service.ts b/backend/src/services/tickets.service.ts index 4674446..8b03843 100644 --- a/backend/src/services/tickets.service.ts +++ b/backend/src/services/tickets.service.ts @@ -8,9 +8,10 @@ import { TicketsCreateResponseDTO } from "../dtos/tickets.dto"; import ticketsModel from "../models/tickets.model"; -import { Tickets, TicketsId } from "../repositories/interfaces/tickets.entity.interface"; +import { TicketsId } from "../repositories/interfaces/tickets.entity.interface"; import ticketsRepository from "../repositories/tickets.repository"; import * as CommonUtils from "../utils/common.utils"; +import { NotificationService } from "./notificiation.service"; class TicketsService { private timeMapTargets: string[]; @@ -41,7 +42,16 @@ class TicketsService { async createTicket(dto: TicketsCreateDTO, files: Express.Multer.File[] | null): Promise { const ticket = await ticketsModel.generateTicketEntity(dto, files); - const result: Tickets = await ticketsRepository.create(ticket); + const result: TicketsResponseExtendedDTO = await ticketsRepository.create(ticket); + const notification = NotificationService.getInstance(); + await notification.sendTicketInfo({ + ticket_id: result.ticket_id, + client_name: result.client_name, + user_email: result.user_email, + option: result.option, + title: result.title, + created_on: result.created_on + }); return ticketsModel.toTicketsCreateResponseDTO(result); } diff --git a/backend/src/utils/enums/mail-source.enum.ts b/backend/src/utils/enums/mail-source.enum.ts deleted file mode 100644 index fd5ec97..0000000 --- a/backend/src/utils/enums/mail-source.enum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type MailSource = 'artdv' | 'tava'; - -export const MailSource = { - ARTDV: 'artdv' as MailSource, - TAVA: 'tava' as MailSource -} \ No newline at end of file diff --git a/backend/src/utils/exceptions/common.exception.ts b/backend/src/utils/exceptions/common.exception.ts index 532acca..86da8a8 100644 --- a/backend/src/utils/exceptions/common.exception.ts +++ b/backend/src/utils/exceptions/common.exception.ts @@ -29,20 +29,6 @@ export class InternalServerException extends CommonException { } } -export class RequestExceedMaxException extends CommonException { - // TODO(yqni13): remove? (SUPPORT-6) as user should not get emails from us, this exception should never reach them - constructor(message: string = 'support-max-email', data?: any) { - super(message, data); - } -} - -export class InvalidSourceException extends CommonException { - // TODO(yqni13): remove? (SUPPORT-6) in combination with no usage for additional encryption? - constructor(message: string = 'support-invalid-source', data?: any) { - super(message, data); - } -} - export class MaintenanceException extends CommonException { constructor(message: MaintenanceMode, data?: any) { super(`support-maintenance-${message}`, data, ErrorStatusCodes.MaintenanceException); diff --git a/backend/src/utils/secrets.utils.ts b/backend/src/utils/secrets.utils.ts index 06f27ae..f76bd6e 100644 --- a/backend/src/utils/secrets.utils.ts +++ b/backend/src/utils/secrets.utils.ts @@ -9,9 +9,8 @@ class Secrets { readonly ADMIN_KEY: string; readonly ENV_MODE: string; readonly PORT: number; - readonly EMAIL_RECEIVER: string; - readonly EMAIL_SENDER: string; - readonly EMAIL_PASS: string; + readonly NOTIFY_ADMIN_ID: string; + readonly NOTIFY_BOT_KEY: string; readonly BETTERSTACK_LOGGING_KEY: string; readonly BETTERSTACK_HOST: string; readonly DB_USER: string; @@ -45,9 +44,8 @@ class Secrets { 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.NOTIFY_ADMIN_ID = this.setNotifyAdminId(); + this.NOTIFY_BOT_KEY = this.setNotifyBotKey(); this.BETTERSTACK_LOGGING_KEY = this.setBetterStackLoggingKey(); this.BETTERSTACK_HOST = this.setBetterStackHost(); this.DB_USER = this.setDbUser(); @@ -114,25 +112,18 @@ class Secrets { return Config.PORT; } - private setEmailReceiver() { - if(!Config.EMAIL_RECEIVER) { - throw new AuthSecretNotFoundException('secret-404-env#EMAIL_RECEIVER'); + private setNotifyAdminId() { + if(!Config.NOTIFY_ADMIN_ID) { + throw new AuthSecretNotFoundException('secret-404-env#NOTIFY_ADMIN_ID'); } - return Config.EMAIL_RECEIVER; + return Config.NOTIFY_ADMIN_ID; } - private setEmailSender() { - if(!Config.EMAIL_SENDER) { - throw new AuthSecretNotFoundException('secret-404-env#EMAIL_SENDER'); + private setNotifyBotKey() { + if(!Config.NOTIFY_BOT_KEY) { + throw new AuthSecretNotFoundException('secret-404-env#NOTIFY_BOT_KEY'); } - return Config.EMAIL_SENDER; - } - - private setEmailPass() { - if(!Config.EMAIL_PASS) { - throw new AuthSecretNotFoundException('secret-404-env#EMAIL_PASS'); - } - return Config.EMAIL_PASS; + return Config.NOTIFY_BOT_KEY; } private setBetterStackLoggingKey = () => { diff --git a/backend/src/validation/schemata/mailing.schema.validation.ts b/backend/src/validation/schemata/mailing.schema.validation.ts deleted file mode 100644 index 6dd39d7..0000000 --- a/backend/src/validation/schemata/mailing.schema.validation.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { body, ValidationChain } from 'express-validator'; -import { CommonExceptionMessage as Message } from '../../utils/enums/common-exception-messages.enum'; - -export const mailingSchema: ValidationChain[] = [ - body('sender') - .trim() - .notEmpty() - .withMessage(Message.REQUIRED), - body('subject') - .trim() - .notEmpty() - .withMessage(Message.REQUIRED), - body('data') - .trim() - .notEmpty() - .withMessage(Message.REQUIRED), - body('body') - .trim() - .notEmpty() - .withMessage(Message.REQUIRED), - body('source') - .trim() - .notEmpty() - .withMessage(Message.REQUIRED), -]; \ No newline at end of file diff --git a/backend/tests/integration-tests/middleware/penalty.integration.test.ts b/backend/tests/integration-tests/middleware/penalty.integration.test.ts index 16033ec..3e91836 100644 --- a/backend/tests/integration-tests/middleware/penalty.integration.test.ts +++ b/backend/tests/integration-tests/middleware/penalty.integration.test.ts @@ -13,6 +13,7 @@ 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"; +import { NotificationService } from "../../../src/services/notificiation.service"; jest.setTimeout(60000); @@ -23,9 +24,11 @@ const testValidUserId = mockId.users.valid[0] as UsersId; describe('Integration-tests (middleware), priority: class PenaltyHandler', () => { let dbTestSetup: DBTestSetup; + let notification: NotificationService; beforeAll(async () => { dbTestSetup = new DBTestSetup(); await dbTestSetup.init(); + notification = NotificationService.getInstance(); MockUtils.disableConsoleMessages(); await runMigrations('penalty.integration.test.ts'); }); @@ -48,6 +51,7 @@ describe('Integration-tests (middleware), priority: class PenaltyHandler', () => }; await dbTestSetup.addTestData(); + jest.spyOn(notification, 'sendPenaltyInfo').mockImplementation(); const _ = await penaltyHandler.apply(testParam_context); const testFn = await clientsService.getClientById(testValidClientId); const testClientsFlagResult = Flag.WARNING; @@ -63,6 +67,7 @@ describe('Integration-tests (middleware), priority: class PenaltyHandler', () => }; await dbTestSetup.addTestData(); + jest.spyOn(notification, 'sendPenaltyInfo').mockImplementation(); const _ = await penaltyHandler.apply(testParam_context); const testFn = await usersService.getUserById(testValidUserId); const testUsersFlagResult = Flag.WARNING; @@ -78,6 +83,7 @@ describe('Integration-tests (middleware), priority: class PenaltyHandler', () => }; await dbTestSetup.addTestData(); + jest.spyOn(notification, 'sendPenaltyInfo').mockImplementation(); const _ = await penaltyHandler.apply(testParam_context); const testFn = await metaService.getMaintenanceMode('support'); const testMaintenanceModeResult = MaintenanceMode.T011; diff --git a/backend/tests/integration-tests/repositories/feedback.integration.test.ts b/backend/tests/integration-tests/repositories/feedback.integration.test.ts index b292274..a70f080 100644 --- a/backend/tests/integration-tests/repositories/feedback.integration.test.ts +++ b/backend/tests/integration-tests/repositories/feedback.integration.test.ts @@ -8,7 +8,7 @@ import { default as mockId } from "../../mock-data/id.mock-data.json"; import { NextFunction, Request, Response } from "express"; import { CommonExceptionMessage } from "../../../src/utils/enums/common-exception-messages.enum"; import { ErrorStatusCodes } from "../../../src/utils/errorStatusCodes.utils"; -import { FeedbackCreateDTO, FeedbackCreateResponseDTO, FeedbackFilterDTO, FeedbackRequestCreateDTO, FeedbackResponseDTO } from '../../../src/dtos/feedback.dto'; +import { FeedbackCreateDTO, FeedbackCreateResponseDTO, FeedbackExtendedResponseDTO, FeedbackFilterDTO, FeedbackRequestCreateDTO, FeedbackResponseDTO } from '../../../src/dtos/feedback.dto'; import { DBTestData } from '../../db-data.setup'; import router from '../../../src/routes/feedback.route'; import feedbackRatingService from '../../../src/services/feedback-rating.service'; @@ -19,6 +19,7 @@ import feedbackRatingRepository from '../../../src/repositories/feedback-rating. 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'; +import { NotificationService } from '../../../src/services/notificiation.service'; jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ authAdmin: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next()) @@ -49,12 +50,16 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { let dbTestData: DBTestData; let dbData_Feedback: any[]; let dbData_FeedbackRating: any[]; + let testValidClientName: string; + let testValidUserEmail: string; let apiUrl: string; beforeAll(async () => { dbTestSetup = new DBTestSetup(); dbTestData = DBTestData.getInstance(); dbData_Feedback = dbTestData.getFeedbackInsertSql().values; dbData_FeedbackRating = dbTestData.getFeedbackRatingInsertSql().values; + testValidClientName = dbTestData.getClientsInsertSql().values[1]; + testValidUserEmail = dbTestData.getUsersInsertSql().values[1]; await dbTestSetup.init(); MockUtils.disableConsoleMessages(); // Surpress multiple messages (migration progress etc). Disable to debug. await runMigrations('feedback.integration.test.ts'); @@ -125,6 +130,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { Number(((dbData_FeedbackRating[2] + testParam_dto.rating) / (dbData_FeedbackRating[1] + 1)).toFixed(1)); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); const testResult: FeedbackCreateResponseDTO = { rating: testParam_dto.rating, @@ -155,6 +161,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { }; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); const testResult: FeedbackCreateResponseDTO = { rating: testParam_dto.rating, @@ -194,6 +201,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { const testTimestamp_update = '2026-01-01T14:00:08.000Z'; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp_update); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); const testResult: FeedbackCreateResponseDTO = { rating: testParam_dto.rating, @@ -230,6 +238,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { jest.spyOn(CommonUtils, 'logError').mockImplementation(); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); jest.spyOn(feedbackRepository, 'upsertInTa').mockRejectedValue(new Error(mockError)); await dbTestSetup.addTestData(); @@ -253,6 +262,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { const mockError = 'FeedbackRating-create-mock-error'; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); jest.spyOn(feedbackRatingRepository, 'createInTa').mockRejectedValue(new Error(mockError)); await dbTestSetup.addTestData(); @@ -277,10 +287,12 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { message: 'test-feedback-message-new-client[1]' }; const testFeedbackId = testValidFeedbackId; - const testResult: FeedbackResponseDTO | null = { + const testResult: FeedbackExtendedResponseDTO | null = { feedback_id: testFeedbackId, client_id: testValidClientId, + client_name: testValidClientName, user_id: testValidUserId, + user_email: testValidUserEmail, rating: dbData_Feedback[2], term_accepted: dbData_Feedback[3], message: dbData_Feedback[4], @@ -291,6 +303,7 @@ describe('Integration-tests (repository), priority: entity Feedback', () => { const mockError = 'support-constraint-feedback'; jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendFeedbackInfo').mockImplementation(); jest.spyOn(feedbackRepository, 'upsertInTa').mockResolvedValue(testResult); await dbTestSetup.addTestData(); diff --git a/backend/tests/integration-tests/repositories/health.integration.test.ts b/backend/tests/integration-tests/repositories/health.integration.test.ts new file mode 100644 index 0000000..3667c2f --- /dev/null +++ b/backend/tests/integration-tests/repositories/health.integration.test.ts @@ -0,0 +1,58 @@ +import { NextFunction, Request, Response } from "express"; +import * as MockUtils from "../../common.test-utils"; +import { DBTestSetup } from "../../db-container.setup"; +import { runMigrations } from '../../db-migrations.setup'; +import request from 'supertest'; +import { HealthCheckExtended } from "../../../src/services/interfaces/health.interface.service"; +import { EnvMode } from "../../../src/utils/enums/env-mode.enum"; + +jest.mock('../../../src/middleware/auth.admin.middleware', () => ({ + authAdmin: jest.fn(() => (req: Request, res: Response, next: NextFunction) => next()) +})); + +import app from '../../../src/app'; + +jest.setTimeout(60000); + +describe('Integration-tests (repository), priority: HealthCheck', () => { + + let dbTestSetup: DBTestSetup; + let apiUrl: string; + beforeAll(async () => { + dbTestSetup = new DBTestSetup(); + await dbTestSetup.init(); + MockUtils.disableConsoleMessages(); // Surpress multiple messages (migration progress etc). Disable to debug. + await runMigrations('health.integration.test.ts'); + apiUrl = '/api/v1/health'; + }); + + beforeEach(async () => { + // Clean tables before each test to fill test data individually. + await dbTestSetup.clearTables(); + }); + + afterAll(async () => { + await dbTestSetup.shutdown(); + }); + + describe('Testing valid fn calls', () => { + + test('Repository process fn checkDatabase(), result: ?', async () => { + const mockResult: Partial = { + status: 200, + db: { + status: 'ok' + }, + environment: EnvMode.TEST, + }; + + await dbTestSetup.addTestData(); + const testResponse = await request(app) + .get(`${apiUrl}/details`); + + expect(testResponse.statusCode).toBe(200); + expect(testResponse.body['db']).toEqual(mockResult.db); + expect(testResponse.body['environment']).toEqual(mockResult.environment); + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/integration-tests/repositories/tickets.integration.test.ts b/backend/tests/integration-tests/repositories/tickets.integration.test.ts index 16f0ec2..f342c78 100644 --- a/backend/tests/integration-tests/repositories/tickets.integration.test.ts +++ b/backend/tests/integration-tests/repositories/tickets.integration.test.ts @@ -26,6 +26,8 @@ import { UsersId } from "../../../src/repositories/interfaces/users.entity.inter import { ClientsId } from "../../../src/repositories/interfaces/clients.entity.interface"; import { TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface"; import ticketsService from "../../../src/services/tickets.service"; +import { NotificationService } from "../../../src/services/notificiation.service"; +import { DBTestData } from "../../db-data.setup"; const testValidTicketId = mockId.tickets.valid[0] as TicketsId; const testValidClientId = mockId.clients.valid[0] as ClientsId; @@ -64,12 +66,16 @@ jest.setTimeout(60000); describe('Integration-tests (repository), priority: entity Tickets', () => { let dbTestSetup: DBTestSetup; + let dbTestData: DBTestData; + let testValidClientName: string; + let testValidUserEmail: string; let apiUrl: string; - const testClientsName = 'TESTCLIENT'; - const testUsersEmail = 'max.mustermann@yqni13.com'; const testNewParam_ticket_id = mockId.tickets.new[0] as TicketsId; beforeAll(async () => { dbTestSetup = new DBTestSetup(); + dbTestData = DBTestData.getInstance(); + testValidClientName = dbTestData.getClientsInsertSql().values[1]; + testValidUserEmail = dbTestData.getUsersInsertSql().values[1]; await dbTestSetup.init(); MockUtils.disableConsoleMessages(); // Surpress multiple messages (migration progress etc). Disable to debug. await runMigrations('tickets.integration.test.ts'); @@ -92,9 +98,9 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { const testResult: TicketsResponseExtendedDTO = { ticket_id: testParam_id, client_id: testValidClientId, - client_name: testClientsName, + client_name: testValidClientName, user_id: testValidUserId, - user_email: testUsersEmail, + user_email: testValidUserEmail, status: TicketStatus.ISSUED, option: TicketOption.SUPPORT, title: 'test-title', @@ -399,6 +405,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(testNewParam_ticket_id); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendTicketInfo').mockImplementation(); const testResult: TicketsCreateResponseDTO = { status: TicketStatus.ISSUED, @@ -431,6 +438,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(testNewParam_ticket_id); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendTicketInfo').mockImplementation(); jest.spyOn(FilesService.prototype, 'transformFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths); @@ -485,6 +493,7 @@ describe('Integration-tests (repository), priority: entity Tickets', () => { jest.spyOn(CommonUtils, "generateUUID").mockReturnValue(testNewParam_ticket_id); jest.spyOn(CommonUtils, "getTimestampUTC").mockReturnValue(testTimestamp); + jest.spyOn(NotificationService.prototype, 'sendTicketInfo').mockImplementation(); jest.spyOn(FilesService.prototype, 'transformFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'uploadFiles').mockImplementation(); jest.spyOn(FilesService.prototype, 'getResourcePaths').mockReturnValue(mockPaths); diff --git a/backend/tests/unit-tests/models/clients.models.test.ts b/backend/tests/unit-tests/models/clients.model.test.ts similarity index 100% rename from backend/tests/unit-tests/models/clients.models.test.ts rename to backend/tests/unit-tests/models/clients.model.test.ts diff --git a/backend/tests/unit-tests/models/health.model.test.ts b/backend/tests/unit-tests/models/health.model.test.ts new file mode 100644 index 0000000..7dfdd77 --- /dev/null +++ b/backend/tests/unit-tests/models/health.model.test.ts @@ -0,0 +1,33 @@ +import healthModel from "../../../src/models/health.model" +import { HealthCheckMemory } from "../../../src/services/interfaces/health.interface.service" + +describe('Unit-tests (model), priority: HealthCheck', () => { + + describe('Priority: fn checkMemory()', () => { + + describe('Testing valid fn calls', () => { + + test('', () => { + const mockResult: HealthCheckMemory = { + heapUsed: '1MB', + heapTotal: '1MB', + rss: '1MB' + }; + + jest.spyOn(process, 'memoryUsage').mockReturnValue({ + heapUsed: 1 * 1024 * 1024, + heapTotal: 1 * 1024 * 1024, + rss: 1 * 1024 * 1024, + external: 0, + arrayBuffers: 0 + }); + + const testFn = healthModel.checkMemory(); + + expect(testFn.heapUsed).toEqual(mockResult.heapUsed); + expect(testFn.heapTotal).toEqual(mockResult.heapTotal); + expect(testFn.rss).toEqual(mockResult.rss); + }) + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/unit-tests/models/rate-limits.models.test.ts b/backend/tests/unit-tests/models/rate-limits.model.test.ts similarity index 100% rename from backend/tests/unit-tests/models/rate-limits.models.test.ts rename to backend/tests/unit-tests/models/rate-limits.model.test.ts diff --git a/backend/tests/unit-tests/models/users.models.test.ts b/backend/tests/unit-tests/models/users.model.test.ts similarity index 100% rename from backend/tests/unit-tests/models/users.models.test.ts rename to backend/tests/unit-tests/models/users.model.test.ts diff --git a/backend/tests/unit-tests/repositories/clients.repositories.test.ts b/backend/tests/unit-tests/repositories/clients.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/clients.repositories.test.ts rename to backend/tests/unit-tests/repositories/clients.repository.test.ts diff --git a/backend/tests/unit-tests/repositories/db.repositories.test.ts b/backend/tests/unit-tests/repositories/db.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/db.repositories.test.ts rename to backend/tests/unit-tests/repositories/db.repository.test.ts diff --git a/backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts b/backend/tests/unit-tests/repositories/feedback-rating.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/feedback-rating.repositories.test.ts rename to backend/tests/unit-tests/repositories/feedback-rating.repository.test.ts diff --git a/backend/tests/unit-tests/repositories/feedback.repositories.test.ts b/backend/tests/unit-tests/repositories/feedback.repository.test.ts similarity index 92% rename from backend/tests/unit-tests/repositories/feedback.repositories.test.ts rename to backend/tests/unit-tests/repositories/feedback.repository.test.ts index 712d5c2..e416e4e 100644 --- a/backend/tests/unit-tests/repositories/feedback.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/feedback.repository.test.ts @@ -5,7 +5,7 @@ import { DBQueryErrorException } from "../../../src/utils/exceptions/db.exceptio 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 { FeedbackExtendedResponseDTO, 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"; @@ -29,6 +29,8 @@ const mockData: Feedback = { last_modified: mockTimestamp, created_on: mockTimestamp }; +const mockClientName = 'TESTCLIENT'; +const mockUserEmail = 'max.mustermann@yqni13.com' const expectExceptionResult = DBQueryErrorException; const mockBoolean = false; @@ -158,7 +160,19 @@ describe('Unit-tests (repository), priority: entity Feedback', () => { const mockValues: any[] = Object.values(mockParam_entity).map(value => value); const sql = `INSERT`; - const mockResult: Feedback = structuredClone(mockData); + const mockResult: FeedbackExtendedResponseDTO = { + feedback_id: mockData.feedback_id, + client_id: mockData.client_id, + client_name: mockClientName, + user_id: mockData.user_id, + user_email: mockUserEmail, + rating: mockParam_entity.rating!, + term_accepted: mockParam_entity.term_accepted!, + message: mockParam_entity.message!, + last_modified: mockTimestamp, + created_on: mockTimestamp, + blocked: false + }; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await feedbackRepository.upsertInTa((mockClient as any), mockParam_entity); diff --git a/backend/tests/unit-tests/repositories/health.repository.test.ts b/backend/tests/unit-tests/repositories/health.repository.test.ts new file mode 100644 index 0000000..a796123 --- /dev/null +++ b/backend/tests/unit-tests/repositories/health.repository.test.ts @@ -0,0 +1,47 @@ +import { DBConnection } from "../../../src/configs/db"; +import healthRepository from "../../../src/repositories/health.repository"; +import * as MockUtils from "../../common.test-utils"; + +jest.mock("../../../src/configs/db", () => { + return { + DBConnection: { + getInstance: jest.fn() + } + } +}); + +const mockBoolean = false; + +describe('Unit-tests (repository), priority: HealthCheck', () => { + + describe('Database tests, priority: fn checkDatabase()', () => { + + describe('Testing valid fn calls', () => { + + test('Return status for positive database connection', async () => { + const mockQuery = `SELECT 1`; + const mockResult = { status: 'ok' }; + const mockClient = MockUtils.mapMockDbClient(mockResult); + const testFn = await healthRepository.checkDatabase(); + + expect(testFn).toEqual(mockResult); + expect(DBConnection.getInstance).toHaveBeenCalled(); + expect(mockClient.query).toHaveBeenCalledWith( + expect.stringContaining(mockQuery), + ); + }) + }) + + describe('Testing invalid fn calls', () => { + + test('Return error result by catch-block', async () => { + const mockErrorMsg = 'test-error'; + const mockResult = { status: 'error', message: mockErrorMsg }; + const _ = MockUtils.mapMockDbClient(mockResult, mockBoolean, mockErrorMsg); + const testFn = await healthRepository.checkDatabase(); + + expect(testFn).toEqual(mockResult); + }) + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/unit-tests/repositories/meta.repositories.test.ts b/backend/tests/unit-tests/repositories/meta.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/meta.repositories.test.ts rename to backend/tests/unit-tests/repositories/meta.repository.test.ts diff --git a/backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts b/backend/tests/unit-tests/repositories/rate-limits.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/rate-limits.repositories.test.ts rename to backend/tests/unit-tests/repositories/rate-limits.repository.test.ts diff --git a/backend/tests/unit-tests/repositories/tickets.repositories.test.ts b/backend/tests/unit-tests/repositories/tickets.repository.test.ts similarity index 95% rename from backend/tests/unit-tests/repositories/tickets.repositories.test.ts rename to backend/tests/unit-tests/repositories/tickets.repository.test.ts index 7b7b90c..d1caf01 100644 --- a/backend/tests/unit-tests/repositories/tickets.repositories.test.ts +++ b/backend/tests/unit-tests/repositories/tickets.repository.test.ts @@ -23,6 +23,8 @@ 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 mockValidClientName = 'TESTCLIENT'; +const mockValidUserEmail = 'max.mustermann@yqni13.com'; const mockTimestamp = '2025-01-01T14:00:04.000Z'; const mockData: Tickets = { ticket_id: mockValidTicketId, @@ -374,7 +376,23 @@ describe('Unit-tests (repository), priority: entity Tickets', () => { test('Return data for created entry, params: = "new-test-message"', async () => { const sql = `INSERT`; const mockValues: any[] = Object.values(mockParam_entity).map(value => value); - const mockResult: Tickets = structuredClone(mockParam_entity); + const mockResult: TicketsResponseExtendedDTO = { + ticket_id: mockParam_entity.ticket_id, + client_id: mockParam_entity.client_id, + client_name: mockValidClientName, + user_id: mockParam_entity.user_id, + user_email: mockValidUserEmail, + status: mockParam_entity.status, + option: mockParam_entity.option, + title: mockParam_entity.title, + message: mockParam_entity.message, + flag: mockParam_entity.flag, + info_browser: mockParam_entity.info_browser, + info_os: mockParam_entity.info_os, + info_device: mockParam_entity.info_device, + last_modified: mockTimestamp, + created_on: mockTimestamp + }; const mockClient = MockUtils.mapMockDbClient(mockResult); const testFn = await ticketsRepository.create(mockParam_entity); diff --git a/backend/tests/unit-tests/repositories/users.repositories.test.ts b/backend/tests/unit-tests/repositories/users.repository.test.ts similarity index 100% rename from backend/tests/unit-tests/repositories/users.repositories.test.ts rename to backend/tests/unit-tests/repositories/users.repository.test.ts diff --git a/backend/tests/unit-tests/services/notification.service.test.ts b/backend/tests/unit-tests/services/notification.service.test.ts new file mode 100644 index 0000000..cba9c02 --- /dev/null +++ b/backend/tests/unit-tests/services/notification.service.test.ts @@ -0,0 +1,152 @@ +import axios from "axios"; +import { TicketsId } from "../../../src/repositories/interfaces/tickets.entity.interface"; +import { NotificationFeedbackParams, NotificationPenaltyParams, NotificationPostParams, NotificationTicketsParams } from "../../../src/services/interfaces/notification.interface.service"; +import { NotificationService } from "../../../src/services/notificiation.service"; +import * as CommonUtils from "../../../src/utils/common.utils"; +import { TicketOption } from "../../../src/utils/enums/ticket-option.enum"; +import { secrets } from "../../../src/utils/secrets.utils"; +import { default as mockId } from "../../mock-data/id.mock-data.json"; +import { FeedbackId } from "../../../src/repositories/interfaces/feedback.entity.interface"; +import { UsersId } from "../../../src/repositories/interfaces/users.entity.interface"; +import { Violation } from "../../../src/utils/enums/violations.enum"; +import { Flag } from "../../../src/utils/enums/flag.enum"; + +describe('Unit-tests (service), priority: class CloudService', () => { + + let notification: NotificationService; + let mockTimestamp: string; + beforeEach(() => { + notification = NotificationService.getInstance(); + mockTimestamp = '2025-01-01T14:01:00.000Z'; + jest.clearAllMocks(); + }) + + describe('Service tests, priority: fn sendTicketInfo()', () => { + + describe('Testing valid fn calls', () => { + + test('Call axios fn post(), params: valid ', async () => { + const mockParam_data: NotificationTicketsParams = { + ticket_id: mockId.tickets.valid[0] as TicketsId, + option: TicketOption.SUPPORT, + title: 'new-test-title', + client_name: 'TESTCLIENT', + user_email: 'max.mustermann@yqni13.com', + created_on: mockTimestamp + }; + jest.spyOn(CommonUtils, 'getTimestampUTC').mockReturnValue(mockTimestamp); + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + + await notification.sendTicketInfo(mockParam_data); + const mockResult_text = (mockAxios.mock.calls[0][1] as any).text; + + Object.values(mockParam_data).forEach((val: any) => { + expect(mockResult_text).toContain(val.toString()); + }); + }) + }) + }) + + describe('Service tests, priority: fn sendFeedbackInfo()', () => { + + describe('Testing valid fn calls', () => { + + test('Call axios fn post(), params: valid ', async () => { + const mockParam_data: NotificationFeedbackParams = { + feedback_id: mockId.feedback.valid[0] as FeedbackId, + rating: 5, + rating_average: 4.9, + term_accepted: true, + client_name: 'TESTCLIENT', + user_email: 'max.mustermann@yqni13.com', + created_on: mockTimestamp + }; + jest.spyOn(CommonUtils, 'getTimestampUTC').mockReturnValue(mockTimestamp); + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + + await notification.sendFeedbackInfo(mockParam_data); + const mockResult_text = (mockAxios.mock.calls[0][1] as any).text; + + Object.values(mockParam_data).forEach((val: any) => { + expect(mockResult_text).toContain(val.toString()); + }); + }) + }) + }) + + describe('Service tests, priority: fn sendPenaltyInfo()', () => { + + describe('Testing valid fn calls', () => { + + test('Call axios fn post(), params: valid ', async () => { + const mockParam_data: NotificationPenaltyParams = { + id: mockId.users.valid[0] as UsersId, + entity: 'Users', + user_email: 'max.mustermann@yqni13.com', + violation: Violation.USERSFLAG, + penalty: Flag.WARNING + }; + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + + await notification.sendPenaltyInfo(mockParam_data); + const mockResult_text = (mockAxios.mock.calls[0][1] as any).text; + + Object.values(mockParam_data).forEach((val: any) => { + expect(mockResult_text).toContain(val.toString()); + }); + }) + }) + }) + + describe('Service tests, priority: fn notify()', () => { + + let mockParam_params: NotificationPostParams; + beforeEach(() => { + mockParam_params = { + text: 'test-text', + logMsg: 'TEST ERROR ON NOTIFY', + logMethod: 'Support_NotificationService_notify' + } + jest.clearAllMocks(); + }) + + describe('Testing valid fn calls', () => { + + test('Call axios fn post(), params: ', async () => { + const mockAdminId = 'valid-test-admin-id'; + const mockBotKey = 'valid-test-bot-key'; + jest.replaceProperty(secrets, 'NOTIFY_ADMIN_ID', mockAdminId); + jest.replaceProperty(secrets, 'NOTIFY_BOT_KEY', mockBotKey); + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + + await (notification as any).notify(mockParam_params); // private fn + + expect(mockAxios).toHaveBeenCalledTimes(1); + expect(mockAxios).toHaveBeenCalledWith( + `https://api.telegram.org/bot${mockBotKey}/sendMessage`, + expect.objectContaining({ chat_id: mockAdminId }) + ); + }) + }) + + describe('Testing invalid fn calls()', () => { + + test('Call axios fn post(), throw exception on mocked api call', async () => { + const mockError = jest.spyOn(CommonUtils, 'logError').mockImplementation(() => {}); + const mockAxiosError = new Error('test-error'); + const mockAxios = jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 }); + + mockAxios.mockRejectedValue(mockAxiosError); + // Notification runs without throwing Error. + await expect((notification as any).notify(mockParam_params)).resolves.not.toThrow(); + + expect(mockError).toHaveBeenCalledTimes(1); + expect(mockError).toHaveBeenCalledWith( + mockParam_params.logMsg, + mockParam_params.logMethod, + mockAxiosError + ); + }) + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/unit-tests/validation/common.validation.test.ts b/backend/tests/unit-tests/validation/common.validation.test.ts index 5fe6df4..e2c586b 100644 --- a/backend/tests/unit-tests/validation/common.validation.test.ts +++ b/backend/tests/unit-tests/validation/common.validation.test.ts @@ -1,7 +1,6 @@ import { MaintenanceMode } from "../../../src/utils/enums/maintenance-mode.enum"; import * as CommonValidators from "../../../src/validation/common.validation"; import { EnvMode } from "../../../src/utils/enums/env-mode.enum"; -import { MailSource } from "../../../src/utils/enums/mail-source.enum"; import { ApiKeyStatus } from "../../../src/utils/enums/api-key-status.enum"; import { UserStatus } from "../../../src/utils/enums/user-status.enum"; import { Flag } from "../../../src/utils/enums/flag.enum"; @@ -21,7 +20,6 @@ describe('Unit-tests (validation), priority: synonym CommonValidators', () => { { enumObj: DemoMode, enumName: 'demoMode' }, { enumObj: EnvMode, enumName: 'envMode' }, { enumObj: Flag, enumName: 'flag' }, - { enumObj: MailSource, enumName: 'mailSource' }, { enumObj: MaintenanceMode, enumName: 'maintenanceMode' }, { enumObj: TicketOption, enumName: 'ticketOption' }, { enumObj: TicketStatus, enumName: 'ticketStatus' }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index db087fe..fd4eab6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,9 +1,26 @@ ## ๐Ÿ“œ $\textsf{\color{salmon}{List\ of\ updates}}$ +### $\textsf{\color{skyblue}2026/03/26}$ + +$\textsf{[v1.5.2\ =>\ {\textbf{\color{brown}v1.6.0}]}}$ app
+- $\textsf{\color{teal}Addition:}$ Added health endpoints to improve health checks (manually + cron-jobs). + +
+ +### $\textsf{\color{skyblue}2026/03/25}$ + +$\textsf{[v1.4.9\ =>\ v1.5.2]}$ app
+- $\textsf{\color{teal}Addition:}$ Added notification service to inform admin about specified events. +- $\textsf{\color{orange}Patch:}$ Updated: + + env variables in connection with notifications (addition/deletion). + + return values of DTO's for entities 'tickets' and 'feedback'. + +
+ ### $\textsf{\color{skyblue}2026/03/14}$ -$\textsf{[v1.4.4\ =>\ {\textbf{\color{brown}v1.4.9}]}}$ app
+$\textsf{[v1.4.4\ =>\ v1.4.9]}$ app
- $\textsf{\color{teal}Addition:}$ Added form-data parser middleware for requests including files. - $\textsf{\color{orange}Patch:}$ Updated: + return types, mapping and handling (part 1). diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d63063c..360cb50 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -5,6 +5,7 @@ | Key | Information | |-----|-------------| | SECRET_ADMIN_KEY | free to create yourself | +| SECRET_NOTIFY_...| create Telegram Bot and use credentials | | 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 |