Skip to content

fix(api): preserve zero project limit - #535

Open
omribz156 wants to merge 2 commits into
ever-co:developfrom
omribz156:codex/ever-traduora-zero-project-limit
Open

omribz156 wants to merge 2 commits into
ever-co:developfrom
omribz156:codex/ever-traduora-zero-project-limit

Conversation

@omribz156

@omribz156 omribz156 commented May 20, 2026

Copy link
Copy Markdown

Fixes #525

What changed

  • Preserves TR_MAX_PROJECTS_PER_USER=0 when loading API config instead of falling back to 100 through 0 || 100.
  • Adds an e2e regression test for the zero-limit case so a non-admin user cannot create the first project when the limit is zero.

Verification

  • git diff --check
  • node -e "const f=(v,d)=>{if(!v)return d;const p=Number.parseInt(v,10);return Number.isNaN(p)?d:p}; console.log(['0','1','',undefined,'abc'].map(v=>String(f(v,100))).join(','))" -> 0,1,100,100,100

I could not complete the focused e2e run locally because yarn --cwd api install --frozen-lockfile timed out before installing cross-env; yarn --cwd api test:e2e ... then failed with cross-env is not recognized. yarn --cwd api build also hit the existing TypeScript 6 baseUrl deprecation before compiling project files.

Before submitting the PR, please make sure you do the following

  1. Contributor license agreement
    For us it's important to have the agreement of our contributors to use their work, whether it be code or documentation. Therefore, we are asking all contributors to sign a contributor license agreement (CLA) as commonly accepted in most open source projects. Just open the pull request and our CLA bot will prompt you briefly.

  2. Please check our contribution guidelines for some help in the process.

This was implemented with Codex assistance, with the patch manually reviewed and kept to the project-limit boundary.


Summary by cubic

Preserves zero project limit set via TR_MAX_PROJECTS_PER_USER=0 in API config instead of defaulting to 100, so users cannot create a project when the limit is zero. Adds a unit test for the config to ensure zero is preserved.

Written for commit cc740d6. Summary will update on new commits. Review in cubic

@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

@omribz156 is attempting to deploy a commit to the Ever Co Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • Bug Fixes

    • Fixed handling of explicit zero values in maximum projects per user configuration to prevent unintended fallback to defaults.
  • Tests

    • Added test coverage for maximum projects per user environment variable configuration.

Walkthrough

The pull request fixes a security bug where setting TR_MAX_PROJECTS_PER_USER=0 failed to limit project creation because 0 was treated as falsy. The config parsing now uses getNumberOrDefault() to preserve zero values, and a unit test validates the corrected behavior.

Changes

Project Limit Zero Handling

Layer / File(s) Summary
Fix maxProjectsPerUser parsing to preserve zero
api/src/config.ts
maxProjectsPerUser parsing switches from parseInt(...) || 100 to getNumberOrDefault(...) to preserve the numeric value 0 instead of treating it as falsy and reverting to the default.
Add unit test for zero project limit parsing
api/src/config.spec.ts
Jest test suite verifies that the config parser correctly handles TR_MAX_PROJECTS_PER_USER='0' by converting it to numeric 0, using environment snapshot and module reset to isolate test state.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A zero once felt lost and small,
But now it stands proud, ten feet tall!
No longer does it hide away,
In defaults bright—it's here to stay. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: preserving zero as a valid project limit value rather than falling back to default.
Description check ✅ Passed The description is directly related to the changeset, explaining what changed, why, and linking to the issue being fixed.
Linked Issues check ✅ Passed The PR addresses issue #525 by implementing the core requirement: preserving TR_MAX_PROJECTS_PER_USER=0 to prevent project creation when limit is zero.
Out of Scope Changes check ✅ Passed All changes in the PR are directly scoped to fixing the zero project limit parsing issue: config.ts uses getNumberOrDefault, and config.spec.ts adds a test for the zero case.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cla-assistant

cla-assistant Bot commented May 20, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where setting TR_MAX_PROJECTS_PER_USER=0 to disallow all project creation was silently ignored because parseInt("0", 10) || 100 evaluates to 100. The fix introduces a getNumberOrDefault helper (already used for throttle settings) that distinguishes an absent/empty variable from an explicit zero.

  • api/src/config.ts: Replaces the parseInt || default pattern with getNumberOrDefault, which treats an empty/absent env var as "use the default" and an explicit "0" as a legitimate zero value.
  • api/test/project.e2e-spec.ts: Adds a regression test that temporarily sets maxProjectsPerUser to 0 via direct config mutation (guarded by try/finally) and asserts that the first project creation attempt returns HTTP 429.

Confidence Score: 4/5

The config fix is correct and narrow in scope; the only concern is the test's direct mutation of the shared config singleton, which is guarded by try/finally.

The core one-line change is correct and the getNumberOrDefault helper is already proven by its existing use for throttle settings. The new e2e test properly exercises the zero-limit path with a confirming GET assertion.

No files require special attention; the change is well-contained to config loading and a single new test case.

Important Files Changed

Filename Overview
api/src/config.ts Replaces `parseInt
api/test/project.e2e-spec.ts Adds a regression test for the zero-limit case by directly mutating the module-level config singleton and restoring it in a finally block. The approach is functional but relies on Jest sequential execution within the file.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /api/v1/projects] --> B{Authenticate user}
    B -- Unauthorized --> C[401 Unauthorized]
    B -- OK --> D{numProjectsCreated >= maxProjectsPerUser?}
    D -- "maxProjectsPerUser = 0" --> E[429 TooManyRequests]
    D -- "maxProjectsPerUser = 100" --> F[Create project in transaction]
    F --> G[201 Created]
Loading

Reviews (1): Last reviewed commit: "fix(api): preserve zero project limit" | Re-trigger Greptile

Comment thread api/test/project.e2e-spec.ts Outdated
Comment on lines +228 to +230
it('/api/v1/projects (POST) should not create a project if the project limit is zero', async () => {
const maxProjectsPerUser = config.maxProjectsPerUser;
config.maxProjectsPerUser = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Mutating the module-level config singleton mid-test works because Jest runs test cases within the same file sequentially, and the controller reads config.maxProjectsPerUser at request time. However, a future move to parallel test workers could leave the value permanently set to 0, causing every subsequent test in the file to reject all project creation requests. The try/finally pattern is the correct approach for plain object properties.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="api/test/project.e2e-spec.ts">

<violation number="1" location="api/test/project.e2e-spec.ts:230">
P2: Regression test mutates in-memory config after the app is already bootstrapped, so it does not cover the original zero-limit config-loading bug.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread api/test/project.e2e-spec.ts Outdated

it('/api/v1/projects (POST) should not create a project if the project limit is zero', async () => {
const maxProjectsPerUser = config.maxProjectsPerUser;
config.maxProjectsPerUser = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Regression test mutates in-memory config after the app is already bootstrapped, so it does not cover the original zero-limit config-loading bug.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/test/project.e2e-spec.ts, line 230:

<comment>Regression test mutates in-memory config after the app is already bootstrapped, so it does not cover the original zero-limit config-loading bug.</comment>

<file context>
@@ -224,6 +225,31 @@ describe('ProjectController (e2e)', () => {
 
+  it('/api/v1/projects (POST) should not create a project if the project limit is zero', async () => {
+    const maxProjectsPerUser = config.maxProjectsPerUser;
+    config.maxProjectsPerUser = 0;
+
+    try {
</file context>

@omribz156 omribz156 May 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, good catch. I replaced that e2e singleton-mutation check with a config-loading unit test in api/src/config.spec.ts, setting TR_MAX_PROJECTS_PER_USER=0 before importing ./config. Verified with git diff --check; local Jest could not run because this checkout has no installed node_modules (cross-env missing), so I'll watch the remote CI run.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/src/config.spec.ts`:
- Around line 14-21: Add tests covering parsing of TR_MAX_PROJECTS_PER_USER
beyond the zero case: create additional it blocks in config.spec.ts that (1) set
process.env.TR_MAX_PROJECTS_PER_USER = '5', call jest.resetModules();
import('./config') and assert config.maxProjectsPerUser === 5, (2) delete
process.env.TR_MAX_PROJECTS_PER_USER, reset modules, import('./config') and
assert it falls back to the default value used by config.maxProjectsPerUser, (3)
set process.env.TR_MAX_PROJECTS_PER_USER = 'invalid', reset modules,
import('./config') and assert it falls back to the default, and optionally (4)
set a negative string like '-1' and assert behavior (default or clamped)
expected by the config implementation; reuse jest.resetModules() and the dynamic
import('./config') pattern to ensure environment changes are picked up.
- Around line 4-12: Move the jest.resetModules() call into a beforeEach block so
each test starts with a clean module cache: add beforeEach(() =>
jest.resetModules()); remove the duplicate jest.resetModules() invocations
inside individual tests and from afterEach; keep the existing afterEach logic
that restores process.env.TR_MAX_PROJECTS_PER_USER using maxProjectsPerUser (and
delete when undefined) so environment cleanup remains intact while module resets
are centralized in beforeEach.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 66b31d81-24aa-45d7-9e09-77386d66affe

📥 Commits

Reviewing files that changed from the base of the PR and between 059d2c8 and cc740d6.

📒 Files selected for processing (1)
  • api/src/config.spec.ts

Comment thread api/src/config.spec.ts
Comment on lines +4 to +12
afterEach(() => {
if (maxProjectsPerUser === undefined) {
delete process.env.TR_MAX_PROJECTS_PER_USER;
} else {
process.env.TR_MAX_PROJECTS_PER_USER = maxProjectsPerUser;
}

jest.resetModules();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider moving jest.resetModules() to beforeEach for idiomatic test structure.

The current pattern calls jest.resetModules() both inside the test (line 15) and in afterEach (line 11). The idiomatic Jest pattern is to place module resets in beforeEach, ensuring every test starts with a clean module cache without requiring individual tests to explicitly reset.

♻️ Proposed refactor to use beforeEach pattern
 describe('config', () => {
   const maxProjectsPerUser = process.env.TR_MAX_PROJECTS_PER_USER;
 
+  beforeEach(() => {
+    jest.resetModules();
+  });
+
   afterEach(() => {
     if (maxProjectsPerUser === undefined) {
       delete process.env.TR_MAX_PROJECTS_PER_USER;
     } else {
       process.env.TR_MAX_PROJECTS_PER_USER = maxProjectsPerUser;
     }
-
-    jest.resetModules();
   });
 
   it('keeps a zero max project limit from the environment', async () => {
-    jest.resetModules();
     process.env.TR_MAX_PROJECTS_PER_USER = '0';
 
     const { config } = await import('./config');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/config.spec.ts` around lines 4 - 12, Move the jest.resetModules()
call into a beforeEach block so each test starts with a clean module cache: add
beforeEach(() => jest.resetModules()); remove the duplicate jest.resetModules()
invocations inside individual tests and from afterEach; keep the existing
afterEach logic that restores process.env.TR_MAX_PROJECTS_PER_USER using
maxProjectsPerUser (and delete when undefined) so environment cleanup remains
intact while module resets are centralized in beforeEach.

Comment thread api/src/config.spec.ts
Comment on lines +14 to +21
it('keeps a zero max project limit from the environment', async () => {
jest.resetModules();
process.env.TR_MAX_PROJECTS_PER_USER = '0';

const { config } = await import('./config');

expect(config.maxProjectsPerUser).toBe(0);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding test cases for other scenarios to increase coverage.

The current test validates the zero case (the specific fix), but additional test cases would increase confidence in the config parsing logic:

  • Positive numbers (e.g., '5'5)
  • Undefined/not set (should use default value)
  • Invalid strings (e.g., 'invalid')
  • Negative numbers (if applicable)
📝 Example additional test cases
it('parses positive max project limit from the environment', async () => {
  process.env.TR_MAX_PROJECTS_PER_USER = '5';

  const { config } = await import('./config');

  expect(config.maxProjectsPerUser).toBe(5);
});

it('uses default when max project limit is not set', async () => {
  delete process.env.TR_MAX_PROJECTS_PER_USER;

  const { config } = await import('./config');

  expect(config.maxProjectsPerUser).toBe(100); // or whatever the default is
});

it('uses default when max project limit is invalid', async () => {
  process.env.TR_MAX_PROJECTS_PER_USER = 'invalid';

  const { config } = await import('./config');

  expect(config.maxProjectsPerUser).toBe(100); // or whatever the default is
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/config.spec.ts` around lines 14 - 21, Add tests covering parsing of
TR_MAX_PROJECTS_PER_USER beyond the zero case: create additional it blocks in
config.spec.ts that (1) set process.env.TR_MAX_PROJECTS_PER_USER = '5', call
jest.resetModules(); import('./config') and assert config.maxProjectsPerUser ===
5, (2) delete process.env.TR_MAX_PROJECTS_PER_USER, reset modules,
import('./config') and assert it falls back to the default value used by
config.maxProjectsPerUser, (3) set process.env.TR_MAX_PROJECTS_PER_USER =
'invalid', reset modules, import('./config') and assert it falls back to the
default, and optionally (4) set a negative string like '-1' and assert behavior
(default or clamped) expected by the config implementation; reuse
jest.resetModules() and the dynamic import('./config') pattern to ensure
environment changes are picked up.

@evereq

evereq commented Jul 15, 2026

Copy link
Copy Markdown
Member

Thanks @omribz156 — reviewed, and the fix is correct and well-scoped. Swapping parseInt(env,10) || 100 for getNumberOrDefault(value, 100) preserves an explicit 0 (the old || 100 treated the valid value 0 as falsy), so TR_MAX_PROJECTS_PER_USER=0 now actually enforces a zero limit. Code is good to merge as-is. ✅

The only remaining blocker is our CAA (Contributor Assignment Agreement — the "CLA" check above from cla-assistant is still not_signed). Please click the sign link in that bot comment to accept the CAA and we'll merge. 🙏

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.

TR_MAX_PROJECTS_PER_USER=0 zero is not equal to zero, security risk.

2 participants