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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

rafgpereira
Copy link

@rafgpereira rafgpereira commented Feb 7, 2025

What kind of change does this PR introduce?

This PR introduces unit tests for the "check" method of the PermissionsService class, increasing test coverage and robustness of the authorization logic. Additionally, it resolves issue #595.

Why was this change needed?

The "check" method is essential for the authorization system, applying complex validation rules—such as channel limits, webhooks, posts per month and specific permissions (ADMIN, TEAM_MEMBERS, etc.)—which, until now, have not been comprehensively tested. The inclusion of these unit tests prevents regressions and ensures that future changes do not compromise the integrity of the system.

  • The tests were designed following the modified condition/decision coverage (MC/DC) standards, an agile approach that reduces the number of tests required (generally n+1 for n conditions) without compromising the robustness and high coverage of logical validation.

  • Jest has also been configured to run unit tests and generate coverage reports, which will facilitate maintenance and ongoing code quality. The library was already installed in the app, just scripts were added to make it functional.

  • Script implemented to enable the execution of tests:

    "test": "jest --coverage"
  • Script for running tests and coverage:
     npm run test

Other information:

This change resolves issue #595, focusing exclusively on improving test coverage and authorization reliability. In the future, we plan to expand testing to other PermissionsService methods and related services. Then, a print of the test execution will be added to demonstrate the coverage achieved.

image

Checklist:

  • I have read the CONTRIBUTING guide.
  • I checked that there were not similar issues or PRs already open for this.
  • This PR fixes just ONE issue (do not include multiple issues or types of change in the same PR) For example, don't try and fix a UI issue and include new dependencies in the same PR.

Summary by CodeRabbit

  • Tests
    • Added a comprehensive testing command that provides detailed coverage reporting, enabling more effective validation of functionality and ensuring our releases consistently meet high quality standards.
    • Enhanced our testing framework by incorporating advanced mocking capabilities, resulting in a more resilient testing environment that supports increased overall project stability and reduces potential issues.
    • Introduced an automated unit testing workflow to streamline testing processes and improve efficiency during code changes.

Copy link

vercel bot commented Feb 7, 2025

@rafgpereira is attempting to deploy a commit to the Listinai Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

coderabbitai bot commented Feb 7, 2025

Walkthrough

This pull request updates the project configuration by modifying the package.json file. It introduces a new script for running tests with coverage reporting through Jest and adds a development dependency for enhanced mocking capabilities. Additionally, a new GitHub Actions workflow file is created to automate unit testing, triggered by specific events related to the project files.

Changes

File Changes
package.json - Added Script: "test": "jest --coverage"
- Added Dev Dependency: "jest-mock-extended": "^4.0.0-beta1"
.github/workflows/unit-test.yaml - Added Workflow: Unit Test Check that runs on push and pull_request events for specific file changes, executing npm run test.

Sequence Diagram(s)

sequenceDiagram
    participant Dev as Developer
    participant GitHub as GitHub Actions
    participant NPM as npm
    participant Jest as Jest Engine

    Dev->>GitHub: Push changes
    GitHub->>NPM: Run workflow
    NPM->>Jest: Execute "jest --coverage"
    Jest->>Dev: Return test results and coverage report
Loading

Poem

Hop, hop, through lines of code I bound,
Happy bits and bytes abound.
Tests now run with watchful eyes,
Mocked dreams under coding skies.
I’m a rabbit with a joyful heart—
Celebrating changes, a delightful art!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a80c5b and 4dd155c.

📒 Files selected for processing (1)
  • .github/workflows/unit-test.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/unit-test.yaml
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Run eslint scanning (frontend)
  • GitHub Check: build (20.17.0)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Run eslint scanning (backend)
  • GitHub Check: build (20.17.0)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/backend/src/services/auth/permissions/permissions.service.test.ts (4)

31-65: Consider adding validation for base test data.

The base subscription and options objects are well-defined, but consider adding validation to ensure the test data remains valid if the underlying models change.

 const baseSubscription = {
   id: 'mock-id',
   organizationId: 'mock-org-id',
   subscriptionTier: 'PRO' as SubscriptionTier,
+  // Add zod schema validation
+  const subscriptionSchema = z.object({
+    id: z.string(),
+    organizationId: z.string(),
+    subscriptionTier: z.nativeEnum(SubscriptionTier),
+    // ... other fields
+  });
+  subscriptionSchema.parse(baseSubscription);

102-178: Expand bypass verification test coverage.

The bypass tests are good but could be enhanced:

  1. Test case for undefined requestedPermissions
  2. Test case for null requestedPermissions
  3. Test case for invalid permission combinations

180-317: Add edge cases for channel permission tests.

Consider adding these test cases:

  1. Test with negative channel limits
  2. Test with undefined channel limits
  3. Test with non-numeric channel limits

318-401: Enhance monthly posts permission test coverage.

The monthly posts tests could be improved with:

  1. Test case for subscription creation date edge cases
  2. Test case for date boundary conditions
  3. Test case for invalid post counts
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43910d8 and 0733fa9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • apps/backend/src/services/auth/permissions/permissions.service.test.ts (1 hunks)
  • package.json (2 hunks)
🔇 Additional comments (4)
apps/backend/src/services/auth/permissions/permissions.service.test.ts (2)

1-8: LGTM! Well-structured imports and mock setup.

The test file follows good practices with clear organization of imports and mock setup.


1-404: Add missing test coverage for other sections.

The test suite focuses on channel and monthly posts permissions but is missing coverage for:

  1. Team members permissions
  2. Webhook permissions
  3. AI feature permissions

Would you like me to generate additional test cases for these sections?

package.json (2)

32-33: LGTM! Test script with coverage reporting.

The addition of the test script with coverage reporting aligns well with the PR objectives.


216-216: LGTM! Jest mock extension dependency.

The addition of jest-mock-extended is appropriate for the mocking needs in the test suite.

@egelhaus egelhaus linked an issue Feb 7, 2025 that may be closed by this pull request
1 task
@egelhaus
Copy link
Collaborator

egelhaus commented Feb 7, 2025

Hey, so if I understand this correctly, this just makes Postiz more secure or what exactly does this do?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
apps/backend/tsconfig.spec.json (1)

13-13: Clarify Redundant Include Entry.

The explicit addition of "src/services/auth/permissions/check-permissions.service.spec.ts" appears redundant because the glob pattern "src/**/*.spec.ts" already covers all spec files under src—including this one. If there is a specific reason for explicitly including this file (e.g., special compilation settings or to override an exclusion elsewhere), please add a clarifying comment. Otherwise, consider removing the duplicate entry.

apps/backend/src/services/auth/permissions/remaining-permissions.service.test.ts (2)

31-48: Consider extracting test fixtures into a separate file.

The test file contains large test fixtures that could be reused across different test files. Consider moving baseSubscription, baseOptions, and baseIntegration into a shared test fixtures file.

+// test-fixtures/permissions.fixtures.ts
+export const baseSubscription = {
+  id: 'mock-id',
+  // ... rest of the subscription object
+};
+
+export const baseOptions = {
+  channel: 10,
+  // ... rest of the options object
+};
+
+export const baseIntegration = {
+  id: 'mock-integration-id',
+  // ... rest of the integration object
+};

Also applies to: 50-65, 67-99


111-123: Consider adding edge case tests for channel quota calculation.

While the current test verifies basic functionality, consider adding tests for:

  • When subscription.totalChannels is undefined
  • When all integrations need refresh
  • When the limit is negative (as seen in pricing[tier].channel for non-FREE tiers)
apps/backend/src/services/auth/permissions/check-permissions.service.spec.ts (2)

180-317: Consider adding error case tests for channel permissions.

While the happy path tests are comprehensive, consider adding tests for error cases:

  • When getIntegrationsList throws an error
  • When getPackageOptions throws an error

318-401: Consider using test.each for monthly posts permission tests.

The monthly posts permission tests have similar structure with different data. Consider using test.each to make the tests more maintainable.

describe('Monthly Posts Permission', () => {
  test.each([
    ['Posts Within Limit', 50, true],
    ['Posts Exceed Limit', 150, false],
  ])('%s', async (name, postCount, expectedResult) => {
    jest.spyOn(service, 'getPackageOptions').mockResolvedValue({
      subscription: baseSubscription,
      options: { ...baseOptions, posts_per_month: 100 },
    });
    jest.spyOn(mockSubscriptionService, 'getSubscription').mockResolvedValue({
      ...baseSubscription,
      createdAt: new Date(),
    });
    jest.spyOn(mockPostsService, 'countPostsFromDay').mockResolvedValue(postCount);
    
    const result = await service.check(
      'mock-org-id',
      new Date(),
      'USER',
      [[AuthorizationActions.Create, Sections.POSTS_PER_MONTH]]
    );
    
    expect(result.can(AuthorizationActions.Create, Sections.POSTS_PER_MONTH)).toBe(expectedResult);
  });
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0733fa9 and fe90cb5.

📒 Files selected for processing (5)
  • apps/backend/src/services/auth/permissions/check-permissions.service.spec.ts (1 hunks)
  • apps/backend/src/services/auth/permissions/permissions.service.ts (1 hunks)
  • apps/backend/src/services/auth/permissions/remaining-permissions.service.test.ts (1 hunks)
  • apps/backend/tsconfig.spec.json (1 hunks)
  • package.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
🔇 Additional comments (3)
apps/backend/src/services/auth/permissions/remaining-permissions.service.test.ts (2)

102-109: LGTM! Test case for unsupported section is well-defined.

The test case properly verifies that unsupported sections return 0 as expected.


124-132: LGTM! Webhook and posts quota tests are comprehensive.

The test cases properly verify the remaining quota calculations for webhooks and posts per month.

Also applies to: 133-146

apps/backend/src/services/auth/permissions/check-permissions.service.spec.ts (1)

102-178: LGTM! Verification bypass tests are comprehensive.

The test cases properly verify the bypass conditions for empty permission lists and missing Stripe key.

Comment on lines 177 to 201
async getRemainingQuota(orgId: string, section: Sections): Promise<number> {
if (section === Sections.CHANNEL) {
const { subscription, options } = await this.getPackageOptions(orgId);
const integrations = await this._integrationService.getIntegrationsList(orgId);
const validChannels = integrations.filter(integration => !integration.refreshNeeded).length;
let limit = options.channel;
if (subscription && subscription.totalChannels > limit) {
limit = subscription.totalChannels;
}
return limit - validChannels;
} else if (section === Sections.WEBHOOKS) {
const { options } = await this.getPackageOptions(orgId);
const totalWebhooks = await this._webhooksService.getTotal(orgId);
return options.webhooks - totalWebhooks;
} else if (section === Sections.POSTS_PER_MONTH) {
const { options } = await this.getPackageOptions(orgId);
const subscriptionInfo = await this._subscriptionService.getSubscription(orgId);
const createdAt = subscriptionInfo?.createdAt || new Date();
const totalMonthsPassed = Math.abs(dayjs(createdAt).diff(dayjs(), 'month'));
const checkFrom = dayjs(createdAt).add(totalMonthsPassed, 'month');
const count = await this._postsService.countPostsFromDay(orgId, checkFrom.toDate());
return options.posts_per_month - count;
}
return 0;
}
Copy link

@coderabbitai coderabbitai bot Feb 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring duplicate logic between check() and getRemainingQuota().

The quota calculation logic is duplicated between check() and getRemainingQuota(). Consider extracting the common logic into private methods to improve maintainability.

+  private async getChannelQuota(orgId: string): Promise<{limit: number, used: number}> {
+    const { subscription, options } = await this.getPackageOptions(orgId);
+    const integrations = await this._integrationService.getIntegrationsList(orgId);
+    const validChannels = integrations.filter(integration => !integration.refreshNeeded).length;
+    let limit = options.channel;
+    if (subscription && subscription.totalChannels > limit) {
+      limit = subscription.totalChannels;
+    }
+    return { limit, used: validChannels };
+  }
+
+  private async getWebhooksQuota(orgId: string): Promise<{limit: number, used: number}> {
+    const { options } = await this.getPackageOptions(orgId);
+    const totalWebhooks = await this._webhooksService.getTotal(orgId);
+    return { limit: options.webhooks, used: totalWebhooks };
+  }
+
+  private async getPostsQuota(orgId: string, created_at: Date): Promise<{limit: number, used: number}> {
+    const { options } = await this.getPackageOptions(orgId);
+    const subscriptionInfo = await this._subscriptionService.getSubscription(orgId);
+    const createdAt = subscriptionInfo?.createdAt || created_at;
+    const totalMonthsPassed = Math.abs(dayjs(createdAt).diff(dayjs(), 'month'));
+    const checkFrom = dayjs(createdAt).add(totalMonthsPassed, 'month');
+    const count = await this._postsService.countPostsFromDay(orgId, checkFrom.toDate());
+    return { limit: options.posts_per_month, used: count };
+  }

   async getRemainingQuota(orgId: string, section: Sections): Promise<number> {
     if (section === Sections.CHANNEL) {
-      const { subscription, options } = await this.getPackageOptions(orgId);
-      const integrations = await this._integrationService.getIntegrationsList(orgId);
-      const validChannels = integrations.filter(integration => !integration.refreshNeeded).length;
-      let limit = options.channel;
-      if (subscription && subscription.totalChannels > limit) {
-        limit = subscription.totalChannels;
-      }
-      return limit - validChannels;
+      const { limit, used } = await this.getChannelQuota(orgId);
+      return limit - used;
     } else if (section === Sections.WEBHOOKS) {
-      const { options } = await this.getPackageOptions(orgId);
-      const totalWebhooks = await this._webhooksService.getTotal(orgId);
-      return options.webhooks - totalWebhooks;
+      const { limit, used } = await this.getWebhooksQuota(orgId);
+      return limit - used;
     } else if (section === Sections.POSTS_PER_MONTH) {
-      const { options } = await this.getPackageOptions(orgId);
-      const subscriptionInfo = await this._subscriptionService.getSubscription(orgId);
-      const createdAt = subscriptionInfo?.createdAt || new Date();
-      const totalMonthsPassed = Math.abs(dayjs(createdAt).diff(dayjs(), 'month'));
-      const checkFrom = dayjs(createdAt).add(totalMonthsPassed, 'month');
-      const count = await this._postsService.countPostsFromDay(orgId, checkFrom.toDate());
-      return options.posts_per_month - count;
+      const { limit, used } = await this.getPostsQuota(orgId, new Date());
+      return limit - used;
     }
     return 0;
   }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has been removed from the PR

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@rafgpereira I can still see the getRemainingQuota method in the file. Could you please clarify if you're referring to a different code change that was removed, or if the removal is still in progress? This will help me provide more accurate assistance.


🏁 Script executed:

#!/bin/bash
# Check if the getRemainingQuota method is still present in the file
ast-grep --pattern 'async getRemainingQuota($_, $_) { $$$ }'

Length of output: 62


🏁 Script executed:

#!/bin/bash
# Search for getRemainingQuota method in the codebase
rg "getRemainingQuota" -A 5

Length of output: 27


🏁 Script executed:

#!/bin/bash
# View the current content of the permissions service file
cat apps/backend/src/services/auth/permissions/permissions.service.ts

Length of output: 5233


@rafgpereira You're absolutely right - I can now see that the getRemainingQuota method has been removed from the file. Thank you for the clarification!

@egelhaus egelhaus linked an issue Feb 12, 2025 that may be closed by this pull request
Copy link
Collaborator

@egelhaus egelhaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rafgpereira , please also update the GitHub Actions Build Workflow so it can be auto tested.

@egelhaus
Copy link
Collaborator

egelhaus commented Feb 12, 2025

@nevo-david Can you please review this?

@rafgpereira
Copy link
Author

Hey @rafgpereira , please also update the GitHub Actions Build Workflow so it can be auto tested.

Sure! I'll do it today.

@egelhaus
Copy link
Collaborator

Or could you please make a new Workflow for this so it's easier for the PR Authors to see what went wrong?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.github/workflows/unit-test.yaml (2)

8-19: Consider expanding path filters for comprehensive coverage.

The path filters should include test files and package-lock.json to ensure tests run when dependencies or test files change.

    paths:
      - package.json
+     - package-lock.json
      - apps/**
      - '!apps/docs/**'
      - libraries/**
+     - '**/*.test.ts'
+     - '**/*.test.tsx'
+     - '**/*.spec.ts'
+     - '**/*.spec.tsx'

25-28: Consider enhancing the test matrix strategy.

To improve test reliability and coverage across different Node.js environments:

  1. Add fail-fast: false to allow all matrix jobs to complete
  2. Consider testing against multiple Node.js LTS versions
    strategy:
+     fail-fast: false
      matrix:
        node-version: ['20.17.0']
+       # Add more LTS versions for better compatibility testing
+       node-version: ['18.x', '20.x']
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4686070 and 2a80c5b.

📒 Files selected for processing (1)
  • .github/workflows/unit-test.yaml (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Run eslint scanning (frontend)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Run eslint scanning (backend)
  • GitHub Check: build (20.17.0)

restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

- run: npm run test
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance test execution and reporting.

The test execution step needs improvements:

  1. Add npm install step
  2. Configure test coverage reporting
  3. Add test results reporting
+     - run: npm ci
      - run: npm run test
+     - name: Upload coverage reports
+       uses: codecov/codecov-action@v3
+       with:
+         token: ${{ secrets.CODECOV_TOKEN }}
+     - name: Test Report
+       uses: dorny/test-reporter@v1
+       if: success() || failure()
+       with:
+         name: Jest Tests
+         path: junit.xml
+         reporter: jest-junit

Also, update your package.json to generate the necessary reports:

{
  "scripts": {
    "test": "jest --coverage --ci --reporters=default --reporters=jest-junit"
  }
}

@egelhaus
Copy link
Collaborator

Hmm, it doesnt seem to exit it.
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants