Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit tests for check method in PermissionsService #596

Closed
wants to merge 10 commits into from
52 changes: 52 additions & 0 deletions .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: Unit Test Check

on:
push:
branches:
- main
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**

pull_request:
paths:
- package.json
- apps/**
- '!apps/docs/**'
- libraries/**

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: ['20.17.0']
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: |
**/package-lock.json
# https://nextjs.org/docs/pages/building-your-application/deploying/ci-build-caching#github-actions
- uses: actions/cache@v4
with:
path: |
~/.npm
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: npm ci
- run: npm run test
403 changes: 403 additions & 0 deletions apps/backend/src/services/auth/permissions/permissions.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,403 @@
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
};

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);
});
});
});
});
31 changes: 31 additions & 0 deletions package-lock.json
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
},
"private": true,
"dependencies": {
@@ -217,6 +218,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "^29.4.1",
"jest-mock-extended": "^4.0.0-beta1",
"jsdom": "~22.1.0",
"postcss": "8.4.38",
"prettier": "^2.6.2",