Merge branch 'dev' into dependabot/npm_and_yarn/remove-markdown-0.6.0

This commit is contained in:
egelhaus 2025-04-02 21:47:54 +02:00 committed by GitHub
commit ee432036a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 865 additions and 136 deletions

16
Jenkinsfile vendored
View File

@ -12,7 +12,7 @@ pipeline {
}
}
stage('Chechout Node.js and npm') {
stage('Check Node.js and npm') {
steps {
script {
sh "node -v"
@ -27,17 +27,25 @@ pipeline {
}
}
stage('Run Unit Tests') {
steps {
sh 'npm test'
}
}
stage('Build Project') {
steps {
sh 'npm run build'
sh 'npm run build 2>&1 | tee build_report.log' // Captures build output
}
}
}
post {
always {
cleanWs(cleanWhenNotBuilt: false,
notFailBuild: true)
junit '**/reports/junit.xml'
archiveArtifacts artifacts: 'reports/**', fingerprint: true
archiveArtifacts artifacts: 'build_report.log', fingerprint: true
cleanWs(cleanWhenNotBuilt: false, notFailBuild: true)
}
success {
echo 'Build completed successfully!'

View File

@ -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')

View File

@ -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<SubscriptionService>();
const mockPostsService = mock<PostsService>();
const mockIntegrationService = mock<IntegrationService>();
const mockWebHookService = mock<WebhooksService>();
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);
});
});
});
});

View File

@ -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": []

View File

@ -38,6 +38,8 @@ export const useValues = (
criteriaMode: 'all',
});
console.log(form.formState.errors);
const getValues = useMemo(() => {
return () => ({ ...form.getValues(), __type: identifier });
}, [form, integration]);

View File

@ -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');
<CheckTikTokValidity picture={props?.values?.[0]?.image?.[0]?.path} />
<Select
label="Who can see this video?"
disabled={isUploadMode}
hideErrors={true}
disabled={isUploadMode}
{...register('privacy_level', {
value: 'PUBLIC_TO_EVERYONE',
})}
@ -141,13 +142,13 @@ disabled={isUploadMode}
</option>
))}
</Select>
<div className="text-[14px] mb-[10px] text-balance">
<div className="text-[14px] mt-[10px] mb-[18px] text-balance">
{`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.`}
</div>
<Select
label="Content posting method"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('content_posting_method', {
value: 'DIRECT_POST',
})}
@ -159,13 +160,31 @@ disabled={isUploadMode}
</option>
))}
</Select>
<Select
hideErrors={true}
label="Auto add music"
{...register('autoAddMusic', {
value: 'no',
})}
>
<option value="">Select</option>
{yesNo.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
<div className="text-[14px] mt-[10px] mb-[24px] text-balance">
This feature available only for photos, it will add a default music that
you can change later.
</div>
<hr className="mb-[15px] border-tableBorder" />
<div className="text-[14px] mb-[10px]">Allow User To:</div>
<div className="flex gap-[40px]">
<Checkbox
variant="hollow"
label="Duet"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('duet', {
value: false,
})}
@ -173,7 +192,7 @@ disabled={isUploadMode}
<Checkbox
label="Stitch"
variant="hollow"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('stitch', {
value: false,
})}
@ -181,7 +200,7 @@ disabled={isUploadMode}
<Checkbox
label="Comments"
variant="hollow"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('comment', {
value: false,
})}
@ -192,7 +211,7 @@ disabled={isUploadMode}
<Checkbox
variant="hollow"
label="Disclose Video Content"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('disclose', {
value: false,
})}
@ -225,12 +244,11 @@ disabled={isUploadMode}
third party, or both.
</div>
</div>
<div className={clsx(!disclose && 'invisible', 'mt-[20px]')}>
<Checkbox
variant="hollow"
label="Your brand"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('brand_organic_toggle', {
value: false,
})}
@ -243,7 +261,7 @@ disabled={isUploadMode}
<Checkbox
variant="hollow"
label="Branded content"
disabled={isUploadMode}
disabled={isUploadMode}
{...register('brand_content_toggle', {
value: false,
})}
@ -290,19 +308,22 @@ export default withProvider(
TikTokDto,
async (items) => {
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

View File

@ -2,4 +2,4 @@ import { getJestProjects } from '@nx/jest';
export default {
projects: getJestProjects(),
};
};

View File

@ -80,7 +80,7 @@ export class BullMqClient extends ClientProxy {
async dispatchEvent(packet: ReadPacket<any>): Promise<any> {
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(

View File

@ -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

View File

@ -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()

View File

@ -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,
})),
},

View File

@ -110,6 +110,7 @@ export interface SocialProvider
ISocialMediaIntegration {
identifier: string;
refreshWait?: boolean;
convertToJPEG?: boolean;
isWeb3?: boolean;
customFields?: () => Promise<
{

View File

@ -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<PostResponse[]> {
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',
}),
}),
}
)

View File

@ -1,6 +1,30 @@
import { Redis } from 'ioredis';
export const ioRedis = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
connectTimeout: 10000
});
// Create a mock Redis implementation for testing environments
class MockRedis {
private data: Map<string, any> = 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

187
package-lock.json generated
View File

@ -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",
@ -198,10 +198,12 @@
"jest": "29.7.0",
"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",
@ -10845,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": {
@ -10866,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": {
@ -24176,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",
@ -30162,6 +30193,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",
@ -30274,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",
@ -39343,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": {
@ -45276,6 +45371,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",
@ -47791,6 +47901,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",

View File

@ -29,7 +29,8 @@
"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": "jest --coverage --detectOpenHandles --reporters=default --reporters=jest-junit"
},
"private": true,
"dependencies": {
@ -71,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",
@ -221,10 +222,12 @@
"jest": "29.7.0",
"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",
@ -235,5 +238,9 @@
},
"volta": {
"node": "20.17.0"
},
"jest-junit": {
"outputDirectory": "./reports",
"outputName": "junit.xml"
}
}

27
reports/junit.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="11" failures="0" errors="0" time="31.615">
<testsuite name="PermissionsService" errors="0" failures="0" skipped="0" timestamp="2025-03-24T05:53:33" time="31.496" tests="11">
<testcase classname="PermissionsService check() Verification Bypass (64) Bypass for Empty List" name="PermissionsService check() Verification Bypass (64) Bypass for Empty List" time="0.01">
</testcase>
<testcase classname="PermissionsService check() Verification Bypass (64) Bypass for Missing Stripe" name="PermissionsService check() Verification Bypass (64) Bypass for Missing Stripe" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Verification Bypass (64) No Bypass" name="PermissionsService check() Verification Bypass (64) No Bypass" time="0.002">
</testcase>
<testcase classname="PermissionsService check() Channel Permission (82/87) All Conditions True" name="PermissionsService check() Channel Permission (82/87) All Conditions True" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Channel Permission (82/87) Channel With Option Limit" name="PermissionsService check() Channel Permission (82/87) Channel With Option Limit" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Channel Permission (82/87) Channel With Subscription Limit" name="PermissionsService check() Channel Permission (82/87) Channel With Subscription Limit" time="0.002">
</testcase>
<testcase classname="PermissionsService check() Channel Permission (82/87) Channel Without Available Limits" name="PermissionsService check() Channel Permission (82/87) Channel Without Available Limits" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Channel Permission (82/87) Section Different from Channel" name="PermissionsService check() Channel Permission (82/87) Section Different from Channel" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Monthly Posts Permission (97/110) Posts Within Limit" name="PermissionsService check() Monthly Posts Permission (97/110) Posts Within Limit" time="0.008">
</testcase>
<testcase classname="PermissionsService check() Monthly Posts Permission (97/110) Posts Exceed Limit" name="PermissionsService check() Monthly Posts Permission (97/110) Posts Exceed Limit" time="0.003">
</testcase>
<testcase classname="PermissionsService check() Monthly Posts Permission (97/110) Section Different with Posts Within Limit" name="PermissionsService check() Monthly Posts Permission (97/110) Section Different with Posts Within Limit" time="0.003">
</testcase>
</testsuite>
</testsuites>

1
version.txt Normal file
View File

@ -0,0 +1 @@
v1.38.1