From e6ec03c658916df842621af13522fc4e0e860226 Mon Sep 17 00:00:00 2001 From: Tom <66643501+FoamToaster@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:33:02 +0000 Subject: [PATCH 01/11] Update bluesky.provider.ts Commented out the alt text line for now until there is a way to set this from within Postiz --- .../src/integrations/social/bluesky.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts index f2d01a74..1535b4ec 100644 --- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts @@ -192,7 +192,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider { $type: 'app.bsky.embed.images', images: images.map((p) => ({ // can be an array up to 4 values - alt: 'image', // the alt text + // alt: 'image', // the alt text - commented this out for now until there is a way to set this from within Postiz image: p.data.blob, })), }, From 3de66b7e68902d1ee12c5b3429baa74d700b5c7f Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 13 Mar 2025 19:45:57 +0700 Subject: [PATCH 02/11] feat: fix scheduler --- libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts index 00d54911..0354b3f3 100644 --- a/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts +++ b/libraries/nestjs-libraries/src/bull-mq-transport-new/client.ts @@ -80,7 +80,7 @@ export class BullMqClient extends ClientProxy { async dispatchEvent(packet: ReadPacket): Promise { console.log('event to dispatch: ', packet); const queue = this.getQueue(packet.pattern); - if (packet.data.options.every) { + if (packet?.data?.options?.every) { const { every, immediately } = packet.data.options; const id = packet.data.id ?? v4(); await queue.upsertJobScheduler( From 8399aee2959fcb9488802201ea7a1b7444e776cc Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sat, 15 Mar 2025 00:57:32 +0700 Subject: [PATCH 03/11] feat: tiktok photos --- .../src/api/routes/marketplace.controller.ts | 21 ++- .../components/launches/helpers/use.values.ts | 2 + .../providers/tiktok/tiktok.provider.tsx | 59 ++++++--- .../database/prisma/posts/posts.service.ts | 122 +++++++++++++----- .../posts/providers-settings/tiktok.dto.ts | 9 +- .../social/social.integrations.interface.ts | 1 + .../integrations/social/tiktok.provider.ts | 94 +++++++++----- 7 files changed, 221 insertions(+), 87 deletions(-) diff --git a/apps/backend/src/api/routes/marketplace.controller.ts b/apps/backend/src/api/routes/marketplace.controller.ts index a9d500e5..afb961e9 100644 --- a/apps/backend/src/api/routes/marketplace.controller.ts +++ b/apps/backend/src/api/routes/marketplace.controller.ts @@ -56,8 +56,12 @@ export class MarketplaceController { connectBankAccount( @GetUserFromRequest() user: User, @Query('country') country: string - ) { - return this._stripeService.createAccountProcess(user.id, user.email, country); + ) { + return this._stripeService.createAccountProcess( + user.id, + user.email, + country + ); } @Post('/item') @@ -126,12 +130,19 @@ export class MarketplaceController { @GetOrgFromRequest() organization: Organization, @Param('id') id: string ) { - const getPost = await this._messagesService.getPost(user.id, organization.id, id); + const getPost = await this._messagesService.getPost( + user.id, + organization.id, + id + ); if (!getPost) { - return ; + return; } - return {...await this._postsService.getPost(getPost.organizationId, id), providerId: getPost.integration.providerIdentifier}; + return { + ...(await this._postsService.getPost(getPost.organizationId, id)), + providerId: getPost.integration.providerIdentifier, + }; } @Post('/posts/:id/revision') diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts index a863e776..3b1e0492 100644 --- a/apps/frontend/src/components/launches/helpers/use.values.ts +++ b/apps/frontend/src/components/launches/helpers/use.values.ts @@ -38,6 +38,8 @@ export const useValues = ( criteriaMode: 'all', }); + console.log(form.formState.errors); + const getValues = useMemo(() => { return () => ({ ...form.getValues(), __type: identifier }); }, [form, integration]); diff --git a/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx index 58d0db42..ffed977a 100644 --- a/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx +++ b/apps/frontend/src/components/launches/providers/tiktok/tiktok.provider.tsx @@ -46,11 +46,11 @@ const contentPostingMethod = [ const yesNo = [ { - value: 'true', + value: 'yes', label: 'Yes', }, { - value: 'false', + value: 'no', label: 'No', }, ]; @@ -120,7 +120,7 @@ const TikTokSettings: FC<{ values?: any }> = (props) => { const disclose = watch('disclose'); const brand_organic_toggle = watch('brand_organic_toggle'); const brand_content_toggle = watch('brand_content_toggle'); -const content_posting_method = watch('content_posting_method'); + const content_posting_method = watch('content_posting_method'); const isUploadMode = content_posting_method === 'UPLOAD'; @@ -129,7 +129,8 @@ const content_posting_method = watch('content_posting_method'); -
+
{`Choose upload without posting if you want to review and edit your content within TikTok's app before publishing. This gives you access to TikTok's built-in editing tools and lets you make final adjustments before posting.`}
+ +
+ This feature available only for photos, it will add a default music that + you can change later. +

Allow User To:
-
{ const [firstItems] = items; - if (items.length !== 1) { return 'Tiktok items should be one'; } - if (items[0].length !== 1) { + if ( + firstItems.length > 1 && + firstItems?.some((p) => p?.path?.indexOf('mp4') > -1) + ) { + return 'Only pictures are supported when selecting multiple items'; + } else if ( + firstItems?.length !== 1 && + firstItems?.[0]?.path?.indexOf('mp4') > -1 + ) { return 'You need one media'; } - if (firstItems[0].path.indexOf('mp4') === -1) { - return 'Item must be a video'; - } - return true; }, 2200 diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index 71db47c0..1404b1ab 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -24,6 +24,10 @@ import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/me import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service'; import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service'; import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto'; +import axios from 'axios'; +import sharp from 'sharp'; +import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory'; +import { Readable } from 'stream'; dayjs.extend(utc); type PostWithConditionals = Post & { @@ -33,6 +37,7 @@ type PostWithConditionals = Post & { @Injectable() export class PostsService { + private storage = UploadFactory.createStorage(); constructor( private _postRepository: PostsRepository, private _workerServiceProducer: BullMqClient, @@ -92,36 +97,90 @@ export class PostsService { return this._postRepository.getPosts(orgId, query); } - async updateMedia(id: string, imagesList: any[]) { + async updateMedia(id: string, imagesList: any[], convertToJPEG = false) { let imageUpdateNeeded = false; - const getImageList = ( - await Promise.all( - imagesList.map(async (p: any) => { - if (!p.path && p.id) { - imageUpdateNeeded = true; - return this._mediaService.getMediaById(p.id); + const getImageList = await Promise.all( + ( + await Promise.all( + imagesList.map(async (p: any) => { + if (!p.path && p.id) { + imageUpdateNeeded = true; + return this._mediaService.getMediaById(p.id); + } + + return p; + }) + ) + ) + .map((m) => { + return { + ...m, + url: + m.path.indexOf('http') === -1 + ? process.env.FRONTEND_URL + + '/' + + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + + m.path + : m.path, + type: 'image', + path: + m.path.indexOf('http') === -1 + ? process.env.UPLOAD_DIRECTORY + m.path + : m.path, + }; + }) + .map(async (m) => { + if (!convertToJPEG) { + return m; } - return p; + if (m.path.indexOf('.png') > -1) { + imageUpdateNeeded = true; + const response = await axios.get(m.url, { + responseType: 'arraybuffer', + }); + + const imageBuffer = Buffer.from(response.data); + + // Use sharp to get the metadata of the image + const buffer = await sharp(imageBuffer) + .jpeg({ quality: 100 }) + .toBuffer(); + + const { path, originalname } = await this.storage.uploadFile({ + buffer, + mimetype: 'image/jpeg', + size: buffer.length, + path: '', + fieldname: '', + destination: '', + stream: new Readable(), + filename: '', + originalname: '', + encoding: '', + }); + + return { + ...m, + name: originalname, + url: + path.indexOf('http') === -1 + ? process.env.FRONTEND_URL + + '/' + + process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + + path + : path, + type: 'image', + path: + path.indexOf('http') === -1 + ? process.env.UPLOAD_DIRECTORY + path + : path, + }; + } + + return m; }) - ) - ).map((m) => { - return { - ...m, - url: - m.path.indexOf('http') === -1 - ? process.env.FRONTEND_URL + - '/' + - process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY + - m.path - : m.path, - type: 'image', - path: - m.path.indexOf('http') === -1 - ? process.env.UPLOAD_DIRECTORY + m.path - : m.path, - }; - }); + ); if (imageUpdateNeeded) { await this._postRepository.updateImages(id, JSON.stringify(getImageList)); @@ -130,7 +189,7 @@ export class PostsService { return getImageList; } - async getPost(orgId: string, id: string) { + async getPost(orgId: string, id: string, convertToJPEG = false) { const posts = await this.getPostsRecursively(id, true, orgId, true); const list = { group: posts?.[0]?.group, @@ -139,7 +198,8 @@ export class PostsService { ...post, image: await this.updateMedia( post.id, - JSON.parse(post.image || '[]') + JSON.parse(post.image || '[]'), + convertToJPEG, ), })) ), @@ -361,7 +421,11 @@ export class PostsService { id: p.id, message: p.content, settings: JSON.parse(p.settings || '{}'), - media: await this.updateMedia(p.id, JSON.parse(p.image || '[]')), + media: await this.updateMedia( + p.id, + JSON.parse(p.image || '[]'), + getIntegration.convertToJPEG + ), })) ), integration diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts index 91eece6c..1c0c4de3 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/tiktok.dto.ts @@ -23,15 +23,18 @@ export class TikTokDto { @IsBoolean() comment: boolean; + @IsIn(['yes', 'no']) + autoAddMusic: 'yes' | 'no'; + @IsBoolean() brand_content_toggle: boolean; @IsBoolean() brand_organic_toggle: boolean; - @IsIn(['true']) - @IsDefined() - isValidVideo: boolean; + // @IsIn(['true']) + // @IsDefined() + // isValidVideo: boolean; @IsIn(['DIRECT_POST', 'UPLOAD']) @IsString() diff --git a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts index 5d43bc84..673e2fd2 100644 --- a/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts +++ b/libraries/nestjs-libraries/src/integrations/social/social.integrations.interface.ts @@ -110,6 +110,7 @@ export interface SocialProvider ISocialMediaIntegration { identifier: string; refreshWait?: boolean; + convertToJPEG?: boolean; isWeb3?: boolean; customFields?: () => Promise< { diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts index 14b4b0c5..45a3e6a8 100644 --- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts @@ -17,6 +17,7 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { identifier = 'tiktok'; name = 'Tiktok'; isBetweenSteps = false; + convertToJPEG = true; scopes = [ 'user.info.basic', 'video.publish', @@ -103,10 +104,10 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { grant_type: 'authorization_code', code_verifier: params.codeVerifier, redirect_uri: `${ - process?.env?.FRONTEND_URL?.indexOf('https') === -1 - ? 'https://redirectmeto.com/' - : '' - }${process?.env?.FRONTEND_URL}/integrations/social/tiktok` + process?.env?.FRONTEND_URL?.indexOf('https') === -1 + ? 'https://redirectmeto.com/' + : '' + }${process?.env?.FRONTEND_URL}/integrations/social/tiktok`, }; const { access_token, refresh_token, scope } = await ( @@ -208,23 +209,27 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { } if (status === 'FAILED') { - throw new BadBody('titok-error-upload', JSON.stringify(post), { - // @ts-ignore - postDetails, - }); + throw new BadBody( + 'titok-error-upload', + JSON.stringify(post), + Buffer.from(JSON.stringify(post)) + ); } await timer(3000); } } - private postingMethod(method: TikTokDto["content_posting_method"]): string { - switch (method) { - case 'UPLOAD': - return '/inbox/video/init/'; - case 'DIRECT_POST': - default: - return '/video/init/'; + private postingMethod( + method: TikTokDto['content_posting_method'], + isPhoto: boolean + ): string { + switch (method) { + case 'UPLOAD': + return isPhoto ? '/content/init/' : '/inbox/video/init/'; + case 'DIRECT_POST': + default: + return isPhoto ? '/content/init/' : '/video/init/'; } } @@ -235,11 +240,15 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { integration: Integration ): Promise { const [firstPost, ...comments] = postDetails; + const { data: { publish_id }, } = await ( await this.fetch( - `https://open.tiktokapis.com/v2/post/publish${this.postingMethod(firstPost.settings.content_posting_method)}`, + `https://open.tiktokapis.com/v2/post/publish${this.postingMethod( + firstPost.settings.content_posting_method, + (firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) === -1 + )}`, { method: 'POST', headers: { @@ -247,21 +256,44 @@ export class TiktokProvider extends SocialAbstract implements SocialProvider { Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ - ...(firstPost.settings.content_posting_method === 'DIRECT_POST' ? { - post_info: { - title: firstPost.message, - privacy_level: firstPost.settings.privacy_level, - disable_duet: !firstPost.settings.duet, - disable_comment: !firstPost.settings.comment, - disable_stitch: !firstPost.settings.stitch, - brand_content_toggle: firstPost.settings.brand_content_toggle, - brand_organic_toggle: firstPost.settings.brand_organic_toggle, - } - } : {}), - source_info: { - source: 'PULL_FROM_URL', - video_url: firstPost?.media?.[0]?.url!, - }, + ...(firstPost.settings.content_posting_method === 'DIRECT_POST' + ? { + post_info: { + title: firstPost.message, + privacy_level: firstPost.settings.privacy_level, + disable_duet: !firstPost.settings.duet, + disable_comment: !firstPost.settings.comment, + disable_stitch: !firstPost.settings.stitch, + brand_content_toggle: + firstPost.settings.brand_content_toggle, + brand_organic_toggle: + firstPost.settings.brand_organic_toggle, + ...((firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) === + -1 + ? { + auto_add_music: + firstPost.settings.autoAddMusic === 'yes', + } + : {}), + }, + } + : {}), + ...((firstPost?.media?.[0]?.url?.indexOf('mp4') || -1) > -1 + ? { + source_info: { + source: 'PULL_FROM_URL', + video_url: firstPost?.media?.[0]?.url!, + }, + } + : { + source_info: { + source: 'PULL_FROM_URL', + photo_cover_index: 1, + photo_images: firstPost.media?.map((p) => p.url), + }, + post_mode: 'DIRECT_POST', + media_type: 'PHOTO', + }), }), } ) From a51175c92a4c60c71289f149aba1d033f0af073e Mon Sep 17 00:00:00 2001 From: Cypher474 Date: Mon, 17 Mar 2025 21:41:15 +0500 Subject: [PATCH 04/11] Adding feature unit tests package.json and also adding unit tests steps in Jenkinsfile. --- Jenkinsfile | 16 +++++++++---- apps/frontend/project.json | 7 ++++++ package-lock.json | 47 ++++++++++++++++++++++++++++++++++++++ package.json | 17 ++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6cbe169c..15523ca6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -12,7 +12,7 @@ pipeline { } } - stage('Chechout Node.js and npm') { + stage('Check Node.js and npm') { steps { script { sh "node -v" @@ -27,6 +27,12 @@ pipeline { } } + stage('Run Unit Tests') { + steps { + sh 'npm run test -- --coverage --reporters=jest-junit' + } + } + stage('Build Project') { steps { sh 'npm run build' @@ -36,14 +42,16 @@ pipeline { post { always { - cleanWs(cleanWhenNotBuilt: false, - notFailBuild: true) + cleanWs(cleanWhenNotBuilt: false, notFailBuild: true) } success { echo 'Build completed successfully!' + junit 'reports/junit.xml' + archiveArtifacts artifacts: 'reports/**', fingerprint: true } failure { echo 'Build failed!' + junit 'reports/junit.xml' // Ensures test results are captured even on failure } } -} +} \ No newline at end of file diff --git a/apps/frontend/project.json b/apps/frontend/project.json index 51be52e4..2e9dea71 100644 --- a/apps/frontend/project.json +++ b/apps/frontend/project.json @@ -46,6 +46,13 @@ "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/frontend/jest.config.ts" + } } }, "tags": [] diff --git a/package-lock.json b/package-lock.json index fac38847..f9f01b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -198,6 +198,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-environment-node": "^29.4.1", + "jest-junit": "^16.0.0", "jsdom": "~22.1.0", "postcss": "8.4.38", "prettier": "^2.6.2", @@ -30162,6 +30163,45 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -47790,6 +47830,13 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 8b45b020..85b59640 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,13 @@ "prisma-reset": "cd ./libraries/nestjs-libraries/src/database/prisma && npx prisma db push --force-reset && npx prisma db push", "docker-build": "./var/docker/docker-build.sh", "docker-create": "./var/docker/docker-create.sh", - "postinstall": "npm run update-plugins && npm run prisma-generate" + "postinstall": "npm run update-plugins && npm run prisma-generate", + "test": "npx nx run-many --target=test --projects=frontend,backend,workers,cron,commands --parallel=5", + "test:frontend": "npx nx run frontend:test", + "test:backend": "npx nx run backend:test", + "test:workers": "npx nx run workers:test", + "test:cron": "npx nx run cron:test", + "test:commands": "npx nx run commands:test" }, "private": true, "dependencies": { @@ -221,6 +227,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-environment-node": "^29.4.1", + "jest-junit": "^16.0.0", "jsdom": "~22.1.0", "postcss": "8.4.38", "prettier": "^2.6.2", @@ -235,5 +242,11 @@ }, "volta": { "node": "20.17.0" - } + }, + "jest": { + "reporters": [ + "default", + ["jest-junit", { "outputDirectory": "reports", "outputName": "junit.xml" }] + ] + } } From 057755fcfa8dfead2e39d9d919606aec07f73c6b Mon Sep 17 00:00:00 2001 From: egelhaus <156946629+egelhaus@users.noreply.github.com> Date: Tue, 18 Mar 2025 07:56:47 +0100 Subject: [PATCH 05/11] Update Jenkinsfile --- Jenkinsfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 15523ca6..471e27a7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -42,16 +42,15 @@ pipeline { post { always { + junit 'reports/junit.xml' + archiveArtifacts artifacts: 'reports/**', fingerprint: true cleanWs(cleanWhenNotBuilt: false, notFailBuild: true) } success { echo 'Build completed successfully!' - junit 'reports/junit.xml' - archiveArtifacts artifacts: 'reports/**', fingerprint: true } failure { echo 'Build failed!' - junit 'reports/junit.xml' // Ensures test results are captured even on failure } } -} \ No newline at end of file +} From 251cf074c767852adb305cbf0367297ef5524e4a Mon Sep 17 00:00:00 2001 From: egelhaus <156946629+egelhaus@users.noreply.github.com> Date: Tue, 18 Mar 2025 10:04:32 +0100 Subject: [PATCH 06/11] Update Jenkinsfile --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 471e27a7..13013971 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,7 +29,7 @@ pipeline { stage('Run Unit Tests') { steps { - sh 'npm run test -- --coverage --reporters=jest-junit' + sh 'npm run build -- --coverage --reporters=jest-junit' } } @@ -44,7 +44,7 @@ pipeline { always { junit 'reports/junit.xml' archiveArtifacts artifacts: 'reports/**', fingerprint: true - cleanWs(cleanWhenNotBuilt: false, notFailBuild: true) + cleanWs(cleanWhenNotBuilt: true, notFailBuild: true) } success { echo 'Build completed successfully!' From 8e6df31dcf97685a9277b8762b548b8eccfcf16c Mon Sep 17 00:00:00 2001 From: hanzalawebdev Date: Mon, 24 Mar 2025 10:59:54 +0500 Subject: [PATCH 07/11] fixing error of unit tests and also update Jenkinsfile --- Jenkinsfile | 6 +- .../permissions/permissions.service.test.ts | 404 ++++++++++++++++++ jest.config.ts | 2 +- .../src/redis/redis.service.ts | 32 +- package-lock.json | 140 ++++-- package.json | 22 +- reports/junit.xml | 27 ++ 7 files changed, 576 insertions(+), 57 deletions(-) create mode 100644 apps/backend/src/services/auth/permissions/permissions.service.test.ts create mode 100644 reports/junit.xml diff --git a/Jenkinsfile b/Jenkinsfile index 15523ca6..5d52a4c9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,7 +29,7 @@ pipeline { stage('Run Unit Tests') { steps { - sh 'npm run test -- --coverage --reporters=jest-junit' + sh 'npm test' } } @@ -46,12 +46,12 @@ pipeline { } success { echo 'Build completed successfully!' - junit 'reports/junit.xml' + junit '**/reports/junit.xml' archiveArtifacts artifacts: 'reports/**', fingerprint: true } failure { echo 'Build failed!' - junit 'reports/junit.xml' // Ensures test results are captured even on failure + junit '**/reports/junit.xml' } } } \ No newline at end of file diff --git a/apps/backend/src/services/auth/permissions/permissions.service.test.ts b/apps/backend/src/services/auth/permissions/permissions.service.test.ts new file mode 100644 index 00000000..d8fed3f8 --- /dev/null +++ b/apps/backend/src/services/auth/permissions/permissions.service.test.ts @@ -0,0 +1,404 @@ +import { mock } from 'jest-mock-extended'; +import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; +import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; +import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service'; +import { PermissionsService } from './permissions.service'; +import { AuthorizationActions, Sections } from './permissions.service'; +import { Period, SubscriptionTier } from '@prisma/client'; + +// Mock of dependent services +const mockSubscriptionService = mock(); +const mockPostsService = mock(); +const mockIntegrationService = mock(); +const mockWebHookService = mock(); + +describe('PermissionsService', () => { + let service: PermissionsService; + + // Initial setup before each test + beforeEach(() => { + process.env.STRIPE_PUBLISHABLE_KEY = 'mock_stripe_key'; + service = new PermissionsService( + mockSubscriptionService, + mockPostsService, + mockIntegrationService, + mockWebHookService + ); + }); + + // Reusable mocks for `getPackageOptions` + const baseSubscription = { + id: 'mock-id', + organizationId: 'mock-org-id', + subscriptionTier: 'PRO' as SubscriptionTier, + identifier: 'mock-identifier', + cancelAt: new Date(), + period: {} as Period, + totalChannels: 5, + isLifetime: false, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + disabled: false, + tokenExpiration: new Date(), + profile: 'mock-profile', + postingTimes: '[]', + lastPostedAt: new Date(), + }; + + const baseOptions = { + channel: 10, + current: 'mock-current', + month_price: 20, + year_price: 200, + posts_per_month: 100, + team_members: true, + community_features: true, + featured_by_gitroom: true, + ai: true, + import_from_channels: true, + image_generator: false, + image_generation_count: 50, + public_api: true, + webhooks: 10, + autoPost: true // Added the missing property + }; + + const baseIntegration = { + id: 'mock-integration-id', + organizationId: 'mock-org-id', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: new Date(), + additionalSettings: '{}', + refreshNeeded: false, + refreshToken: 'mock-refresh-token', + name: 'Mock Integration', + internalId: 'mock-internal-id', + picture: 'mock-picture-url', + providerIdentifier: 'mock-provider', + token: 'mock-token', + type: 'social', + inBetweenSteps: false, + disabled: false, + tokenExpiration: new Date(), + profile: 'mock-profile', + postingTimes: '[]', + lastPostedAt: new Date(), + customInstanceDetails: 'mock-details', + customerId: 'mock-customer-id', + rootInternalId: 'mock-root-id', + customer: { + id: 'mock-customer-id', + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: new Date(), + name: 'Mock Customer', + orgId: 'mock-org-id', + }, + }; + + describe('check()', () => { + describe('Verification Bypass (64)', () => { + + it('Bypass for Empty List', async () => { + // Setup: STRIPE_PUBLISHABLE_KEY exists and requestedPermission is empty + + // Execution: call the check method with an empty list of permissions + const result = await service.check( + 'mock-org-id', + new Date(), + 'ADMIN', + [] // empty requestedPermission + ); + + // Verification: not requested, no authorization + expect(result.cannot(AuthorizationActions.Create, Sections.CHANNEL)).toBe(true); + }); + + it('Bypass for Missing Stripe', async () => { + // Setup: STRIPE_PUBLISHABLE_KEY does not exist + process.env.STRIPE_PUBLISHABLE_KEY = undefined; + // Necessary mock to avoid undefined filter error + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false } + ]); + // Mock of getPackageOptions (even if not used due to bypass) + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: baseSubscription, + options: baseOptions, + }); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Read, Sections.CHANNEL], + [AuthorizationActions.Create, Sections.AI] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should allow all requested actions due to the absence of the Stripe key + expect(result.can(AuthorizationActions.Read, Sections.CHANNEL)).toBe(true); + expect(result.can(AuthorizationActions.Create, Sections.AI)).toBe(true); + }); + + it('No Bypass', async () => { + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Read, Sections.CHANNEL], + [AuthorizationActions.Create, Sections.AI] + ]; + // Mock of getPackageOptions to force a scenario without permissions + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 0 }, + options: { + ...baseOptions, + channel: 0, + ai: false + }, + }); + // Mock of getIntegrationsList for the channel scenario + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false } + ]); + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should not allow the requested actions as there is no bypass + expect(result.can(AuthorizationActions.Read, Sections.CHANNEL)).toBe(false); + expect(result.can(AuthorizationActions.Create, Sections.AI)).toBe(false); + }); + }); + + describe('Channel Permission (82/87)', () => { + it('All Conditions True', async () => { + // Mock of getPackageOptions to set channel limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 10 }, + options: { ...baseOptions, channel: 10 }, + }); + + // Mock of getIntegrationsList to set existing channels + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + ]); + + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.CHANNEL] + ]; + + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe(true); + }); + + it('Channel With Option Limit', async () => { + // Mock of getPackageOptions to set channel limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 3 }, + options: { ...baseOptions, channel: 10 }, + }); + // Mock of getIntegrationsList to set existing channels + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + ]); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.CHANNEL] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe(true); + }); + + it('Channel With Subscription Limit', async () => { + // Mock of getPackageOptions to set channel limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 10 }, + options: { ...baseOptions, channel: 3 }, + }); + // Mock of getIntegrationsList to set existing channels + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + ]); + + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.CHANNEL] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe(true); + }); + it('Channel Without Available Limits', async () => { + // Mock of getPackageOptions to set channel limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 3 }, + options: { ...baseOptions, channel: 3 }, + }); + // Mock of getIntegrationsList to set existing channels + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + ]); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.CHANNEL] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should not allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe(false); + }); + it('Section Different from Channel', async () => { + // Mock of getPackageOptions to set channel limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: { ...baseSubscription, totalChannels: 10 }, + options: { ...baseOptions, channel: 10 }, + }); + // Mock of getIntegrationsList to set existing channels + jest.spyOn(mockIntegrationService, 'getIntegrationsList').mockResolvedValue([ + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + { ...baseIntegration, refreshNeeded: false }, + ]); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.AI] // Requesting permission for AI instead of CHANNEL + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should not allow the requested action in CHANNEL + expect(result.can(AuthorizationActions.Create, Sections.CHANNEL)).toBe(false); + }); + }); + describe('Monthly Posts Permission (97/110)', () => { + it('Posts Within Limit', async () => { + // Mock of getPackageOptions to set post limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: baseSubscription, + options: { ...baseOptions, posts_per_month: 100 }, + }); + // Mock of getSubscription + jest.spyOn(mockSubscriptionService, 'getSubscription').mockResolvedValue({ + ...baseSubscription, + createdAt: new Date(), + }); + // Mock of countPostsFromDay to return quantity within the limit + jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(50); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.POSTS_PER_MONTH] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH)).toBe(true); + }); + it('Posts Exceed Limit', async () => { + // Mock of getPackageOptions to set post limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: baseSubscription, + options: { ...baseOptions, posts_per_month: 100 }, + }); + // Mock of getSubscription + jest.spyOn(mockSubscriptionService, 'getSubscription').mockResolvedValue({ + ...baseSubscription, + createdAt: new Date(), + }); + // Mock of countPostsFromDay to return quantity above the limit + jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(150); + + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.POSTS_PER_MONTH] + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should not allow the requested action + expect(result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH)).toBe(false); + }); + it('Section Different with Posts Within Limit', async () => { + // Mock of getPackageOptions to set post limits + jest.spyOn(service, 'getPackageOptions').mockResolvedValue({ + subscription: baseSubscription, + options: { ...baseOptions, posts_per_month: 100 }, + }); + // Mock of getSubscription + jest.spyOn(mockSubscriptionService, 'getSubscription').mockResolvedValue({ + ...baseSubscription, + createdAt: new Date(), + }); + // Mock of countPostsFromDay to return quantity within the limit + jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(50); + // List of requested permissions + const requestedPermissions: Array<[AuthorizationActions, Sections]> = [ + [AuthorizationActions.Create, Sections.AI] // Requesting permission for AI instead of POSTS_PER_MONTH + ]; + // Execution: call the check method + const result = await service.check( + 'mock-org-id', + new Date(), + 'USER', + requestedPermissions + ); + // Verification: should not allow the requested action in POSTS_PER_MONTH + expect(result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH)).toBe(false); + }); + }); + }); +}); \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts index d0dbd1b8..907654eb 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,4 +2,4 @@ import { getJestProjects } from '@nx/jest'; export default { projects: getJestProjects(), -}; +}; \ No newline at end of file diff --git a/libraries/nestjs-libraries/src/redis/redis.service.ts b/libraries/nestjs-libraries/src/redis/redis.service.ts index 26238444..2729eeb6 100644 --- a/libraries/nestjs-libraries/src/redis/redis.service.ts +++ b/libraries/nestjs-libraries/src/redis/redis.service.ts @@ -1,6 +1,30 @@ import { Redis } from 'ioredis'; -export const ioRedis = new Redis(process.env.REDIS_URL!, { - maxRetriesPerRequest: null, - connectTimeout: 10000 -}); \ No newline at end of file +// Create a mock Redis implementation for testing environments +class MockRedis { + private data: Map = new Map(); + + async get(key: string) { + return this.data.get(key); + } + + async set(key: string, value: any) { + this.data.set(key, value); + return 'OK'; + } + + async del(key: string) { + this.data.delete(key); + return 1; + } + + // Add other Redis methods as needed for your tests +} + +// Use real Redis if REDIS_URL is defined, otherwise use MockRedis +export const ioRedis = process.env.REDIS_URL + ? new Redis(process.env.REDIS_URL, { + maxRetriesPerRequest: null, + connectTimeout: 10000 + }) + : (new MockRedis() as unknown as Redis); // Type cast to Redis to maintain interface compatibility diff --git a/package-lock.json b/package-lock.json index f9f01b3c..81767ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "@nx/webpack": "19.7.2", "@nx/workspace": "19.7.2", "@postiz/wallets": "^0.0.1", - "@prisma/client": "^6.4.1", + "@prisma/client": "^6.5.0", "@solana/wallet-adapter-react": "^0.15.35", "@solana/wallet-adapter-react-ui": "^0.9.35", "@swc/helpers": "0.5.13", @@ -199,10 +199,11 @@ "jest-environment-jsdom": "29.7.0", "jest-environment-node": "^29.4.1", "jest-junit": "^16.0.0", + "jest-mock-extended": "^4.0.0-beta1", "jsdom": "~22.1.0", "postcss": "8.4.38", "prettier": "^2.6.2", - "prisma": "^5.8.1", + "prisma": "^6.5.0", "react-refresh": "^0.10.0", "sass": "1.62.1", "ts-jest": "^29.1.0", @@ -10846,9 +10847,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz", - "integrity": "sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz", + "integrity": "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -10867,49 +10868,65 @@ } } }, + "node_modules/@prisma/config": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.5.0.tgz", + "integrity": "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + } + }, "node_modules/@prisma/debug": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", - "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", - "devOptional": true + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.5.0.tgz", + "integrity": "sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", - "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.5.0.tgz", + "integrity": "sha512-FVPQYHgOllJklN9DUyujXvh3hFJCY0NX86sDmBErLvoZjy2OXGiZ5FNf3J/C4/RZZmCypZBYpBKEhx7b7rEsdw==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/fetch-engine": "5.22.0", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/fetch-engine": "6.5.0", + "@prisma/get-platform": "6.5.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", - "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", - "devOptional": true + "version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60.tgz", + "integrity": "sha512-iK3EmiVGFDCmXjSpdsKGNqy9hOdLnvYBrJB61far/oP03hlIxrb04OWmDjNTwtmZ3UZdA5MCvI+f+3k2jPTflQ==", + "devOptional": true, + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", - "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.5.0.tgz", + "integrity": "sha512-3LhYA+FXP6pqY8FLHCjewyE8pGXXJ7BxZw2rhPq+CZAhvflVzq4K8Qly3OrmOkn6wGlz79nyLQdknyCG2HBTuA==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0", - "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", - "@prisma/get-platform": "5.22.0" + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/get-platform": "6.5.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", - "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.5.0.tgz", + "integrity": "sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.22.0" + "@prisma/debug": "6.5.0" } }, "node_modules/@project-serum/sol-wallet-adapter": { @@ -24177,6 +24194,19 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -30314,6 +30344,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-mock-extended": { + "version": "4.0.0-beta1", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-4.0.0-beta1.tgz", + "integrity": "sha512-MYcI0wQu3ceNhqKoqAJOdEfsVMamAFqDTjoLN5Y45PAG3iIm4WGnhOu0wpMjlWCexVPO71PMoNir9QrGXrnIlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-essentials": "^10.0.2" + }, + "peerDependencies": { + "@jest/globals": "^28.0.0 || ^29.0.0", + "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", + "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -39383,22 +39428,32 @@ "dev": true }, "node_modules/prisma": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", - "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.5.0.tgz", + "integrity": "sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.22.0" + "@prisma/config": "6.5.0", + "@prisma/engines": "6.5.0" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=16.13" + "node": ">=18.18" }, "optionalDependencies": { "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/prismjs": { @@ -45315,6 +45370,21 @@ "node": ">=6.10" } }, + "node_modules/ts-essentials": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.0.4.tgz", + "integrity": "sha512-lwYdz28+S4nicm+jFi6V58LaAIpxzhg9rLdgNC1VsdP/xiFBseGhF1M/shwCk6zMmwahBZdXcl34LVHrEang3A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/package.json b/package.json index 85b59640..5ee53a5b 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,7 @@ "docker-build": "./var/docker/docker-build.sh", "docker-create": "./var/docker/docker-create.sh", "postinstall": "npm run update-plugins && npm run prisma-generate", - "test": "npx nx run-many --target=test --projects=frontend,backend,workers,cron,commands --parallel=5", - "test:frontend": "npx nx run frontend:test", - "test:backend": "npx nx run backend:test", - "test:workers": "npx nx run workers:test", - "test:cron": "npx nx run cron:test", - "test:commands": "npx nx run commands:test" + "test": "jest --coverage --detectOpenHandles --reporters=default --reporters=jest-junit" }, "private": true, "dependencies": { @@ -77,7 +72,7 @@ "@nx/webpack": "19.7.2", "@nx/workspace": "19.7.2", "@postiz/wallets": "^0.0.1", - "@prisma/client": "^6.4.1", + "@prisma/client": "^6.5.0", "@solana/wallet-adapter-react": "^0.15.35", "@solana/wallet-adapter-react-ui": "^0.9.35", "@swc/helpers": "0.5.13", @@ -228,10 +223,11 @@ "jest-environment-jsdom": "29.7.0", "jest-environment-node": "^29.4.1", "jest-junit": "^16.0.0", + "jest-mock-extended": "^4.0.0-beta1", "jsdom": "~22.1.0", "postcss": "8.4.38", "prettier": "^2.6.2", - "prisma": "^5.8.1", + "prisma": "^6.5.0", "react-refresh": "^0.10.0", "sass": "1.62.1", "ts-jest": "^29.1.0", @@ -243,10 +239,8 @@ "volta": { "node": "20.17.0" }, - "jest": { - "reporters": [ - "default", - ["jest-junit", { "outputDirectory": "reports", "outputName": "junit.xml" }] - ] - } + "jest-junit": { + "outputDirectory": "./reports", + "outputName": "junit.xml" + } } diff --git a/reports/junit.xml b/reports/junit.xml new file mode 100644 index 00000000..2239d502 --- /dev/null +++ b/reports/junit.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 742d60429c93331ad1b1b6c03c32a4f981a85801 Mon Sep 17 00:00:00 2001 From: Muhammad Hanzala Ali Date: Mon, 24 Mar 2025 12:01:47 +0500 Subject: [PATCH 08/11] Update Jenkinsfile --- Jenkinsfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5d52a4c9..3d30ee91 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -42,16 +42,16 @@ pipeline { post { always { + junit '**/reports/junit.xml' + archiveArtifacts artifacts: 'reports/**', fingerprint: true cleanWs(cleanWhenNotBuilt: false, notFailBuild: true) } success { echo 'Build completed successfully!' - junit '**/reports/junit.xml' - archiveArtifacts artifacts: 'reports/**', fingerprint: true } failure { echo 'Build failed!' junit '**/reports/junit.xml' } } -} \ No newline at end of file +} From 8cbf069f70da6be9ba8d117bed386c236b7c1942 Mon Sep 17 00:00:00 2001 From: egelhaus <156946629+egelhaus@users.noreply.github.com> Date: Mon, 24 Mar 2025 08:19:18 +0100 Subject: [PATCH 09/11] Update Jenkinsfile --- Jenkinsfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3d30ee91..57a77a63 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -51,7 +51,6 @@ pipeline { } failure { echo 'Build failed!' - junit '**/reports/junit.xml' } } } From ac3386f58b7346cf56c1ef40763b96cb16c68f21 Mon Sep 17 00:00:00 2001 From: hanzalawebdev Date: Thu, 27 Mar 2025 10:31:52 +0500 Subject: [PATCH 10/11] Add functionality of build reports in CI/CD in Jenkinsfile. --- Jenkinsfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 57a77a63..d8a1fcc4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,7 +35,7 @@ pipeline { stage('Build Project') { steps { - sh 'npm run build' + sh 'npm run build 2>&1 | tee build_report.log' // Captures build output } } } @@ -44,6 +44,7 @@ pipeline { always { junit '**/reports/junit.xml' archiveArtifacts artifacts: 'reports/**', fingerprint: true + archiveArtifacts artifacts: 'build_report.log', fingerprint: true cleanWs(cleanWhenNotBuilt: false, notFailBuild: true) } success { From 0f7272097acc3f9a5d47e1c9d4b617664f83bb89 Mon Sep 17 00:00:00 2001 From: egelhaus <156946629+egelhaus@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:15:53 +0200 Subject: [PATCH 11/11] v1.38.1 --- version.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 version.txt diff --git a/version.txt b/version.txt new file mode 100644 index 00000000..cbec0de0 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +v1.38.1