Skip to content

fix(flashcard): clone Date object in getFlashcardStats to prevent side-effect mutation (#1196) - #1526

Open
suhaniiz wants to merge 5 commits into
Canopus-Labs:mainfrom
suhaniiz:fix/date-mutation-issue-1196
Open

fix(flashcard): clone Date object in getFlashcardStats to prevent side-effect mutation (#1196)#1526
suhaniiz wants to merge 5 commits into
Canopus-Labs:mainfrom
suhaniiz:fix/date-mutation-issue-1196

Conversation

@suhaniiz

@suhaniiz suhaniiz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📝 Pull Request Description

Related Issue

Closes #1196

Summary

In controllers/flashcard.controller.js, calling now.setHours(0, 0, 0, 0) mutated the original now Date object in-place. As a result, now was prematurely modified to midnight, causing any subsequent checks or parallel references to evaluate against the start of the day rather than the actual current timestamp.

This fix creates a clone (const startOfDay = new Date(now)) before applying .setHours(0, 0, 0, 0), preserving now as the accurate current timestamp throughout the request.


Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature
  • ♻️ Refactoring
  • 📝 Documentation update
  • 🎨 UI/UX improvement
  • 🔥 Other(please describe) ______

How Has This Been Tested?

Describe the testing steps performed.

  • Invoked GET /api/flashcards/stats and logged now before and after startOfDay calculation to verify now remains unmutated.
  • Verified that dueCount and reviewedToday MongoDB queries return accurate calculations.

Screenshots (if applicable)

N/A (Backend logic fix)


Checklist

  • My code follows the project's guidelines
  • I have tested my changes
  • I have updated documentation where necessary
  • I have linked the related issue
  • My changes do not introduce new warnings or errors

Looks good to me. Ready to merge.

@suhaniiz

suhaniiz commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Hey! @KaranUnique Just a heads-up regarding issue #1196 (Side-effect mutation of Date object in getFlashcardStats query). The issue was listed as closed on GitHub, but the original code was still mutating the now Date reference in-place in controllers/flashcard.controller.js.

I’ve opened this PR which properly clones now before running .setHours(0, 0, 0, 0). The fix is tested and ready for review whenever you get a chance!

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Flashcard date handling

Layer / File(s) Summary
Flashcard statistics date calculation
backend/controllers/flashcardController.js
getFlashcardStats calculates startOfDay from a copied date and preserves the original timestamp.

Job processing

Layer / File(s) Summary
Country validation and cache upsert
backend/controllers/jobController.js
Adzuna countries use a supported-country whitelist. Job cache writes use duplicate-key-safe upserts. getJobs validates requested countries and uses raw role values in cache keys.
Cache refresh resilience
backend/controllers/jobController.js
Scheduled refresh uses validated country values, the shared cache helper, and per-role error handling.

Authentication API paths

Layer / File(s) Summary
Password recovery path map
frontend/src/utils/apiPaths.js
API_PATHS.AUTH now includes forgot-password and reset-password endpoints.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant getJobs
  participant AdzunaAPI
  participant JobCache

  Client->>getJobs: Request jobs with role and country
  getJobs->>getJobs: Validate country and build cache key
  getJobs->>JobCache: Read cached jobs
  getJobs->>AdzunaAPI: Fetch jobs for validated country
  AdzunaAPI-->>getJobs: Return job results
  getJobs->>JobCache: Upsert job results
  getJobs-->>Client: Return jobs
Loading

Possibly related PRs

Suggested reviewers: gitguru-sudo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning jobController.js and apiPaths.js contain changes unrelated to the requirements of issue #1196. Remove or separate the jobController.js and apiPaths.js changes into PRs that address their respective requirements.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: cloning the Date object in getFlashcardStats to prevent mutation.
Linked Issues check ✅ Passed The flashcard change clones now before normalizing startOfDay, which satisfies issue #1196.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Actionable comments posted: 3

🤖 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 `@backend/controllers/jobController.js`:
- Around line 94-100: Update the invalid-country test in
jobCache.boundedKeys.unit.test.js to expect an HTTP 400 response for
country=USA!!, and verify that no cache write is performed instead of expecting
a backend|in cache key.
- Around line 7-15: Normalize ADZUNA_COUNTRY at initialization by trimming it,
validating it against SUPPORTED_COUNTRIES, and falling back to "in" when invalid
or blank. Update getJobs and refreshJobCache to use this single validated
effective country so both paths share identical configuration behavior.
- Around line 108-109: Update backend/controllers/jobController.js lines 108-109
to pass the requested or session role through normalizeRole and fall back to the
default role when normalization fails; retain the normalized role for cacheKey
generation. At lines 137-141, normalize every stored role with normalizeRole and
skip entries that fail normalization before invoking Adzuna.
🪄 Autofix

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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 68f08077-c2ec-4d8c-9486-8a0631f64d67

📥 Commits

Reviewing files that changed from the base of the PR and between d8f83d7 and dd9c627.

📒 Files selected for processing (3)
  • backend/controllers/flashcardController.js
  • backend/controllers/jobController.js
  • frontend/src/utils/apiPaths.js

Comment on lines +7 to +15
const ADZUNA_COUNTRY = (process.env.ADZUNA_COUNTRY || "in").toLowerCase();
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;

// The Jobs feature is optional: without Adzuna credentials it stays dormant
// instead of crashing the server or spamming failed API calls.
// Supported Adzuna country codes
const SUPPORTED_COUNTRIES = new Set([
"gb", "us", "in", "ca", "au", "de", "fr", "br", "za",
"at", "be", "ch", "es", "it", "nl", "nz", "pl", "ru", "sg"
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use one validated effective default country.

If ADZUNA_COUNTRY is invalid or contains whitespace, getJobs returns HTTP 400 even when the client does not send country. refreshJobCache instead falls back to "in", so the API and refresh paths use different configuration behavior.

Trim and validate the configured value at initialization. Use the same validated fallback in both paths.

🤖 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 `@backend/controllers/jobController.js` around lines 7 - 15, Normalize
ADZUNA_COUNTRY at initialization by trimming it, validating it against
SUPPORTED_COUNTRIES, and falling back to "in" when invalid or blank. Update
getJobs and refreshJobCache to use this single validated effective country so
both paths share identical configuration behavior.

Comment on lines +94 to +100
// Validate country parameter if explicitly provided
let country = req.query.country ? String(req.query.country).toLowerCase() : ADZUNA_COUNTRY;
if (!SUPPORTED_COUNTRIES.has(country)) {
return res.status(400).json({
message: `Invalid or unsupported country code: "${req.query.country}". Supported codes are: ${Array.from(SUPPORTED_COUNTRIES).join(", ")}`,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the invalid-country unit test.

backend/tests/jobCache.boundedKeys.unit.test.js expects country=USA!! to fall back to backend|in. This handler now returns HTTP 400 before it creates a cache key. The existing test will fail.

Change the test to assert the HTTP 400 response and that no cache write occurs.

🤖 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 `@backend/controllers/jobController.js` around lines 94 - 100, Update the
invalid-country test in jobCache.boundedKeys.unit.test.js to expect an HTTP 400
response for country=USA!!, and verify that no cache write is performed instead
of expecting a backend|in cache key.

Comment on lines +108 to +109
const role = req.query.role || latestSession?.role || "software engineer";
const cacheKey = `${role.toLowerCase()}|${country}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restore role normalization in both cache-write paths.

Both paths bypass normalizeRole. Raw roles can create unbounded cache keys and bypass the existing bounded-key contract.

  • backend/controllers/jobController.js#L108-L109: normalize the requested or session role, then use the default role when normalization fails.
  • backend/controllers/jobController.js#L137-L141: normalize each stored role and skip values that fail normalization before calling Adzuna.
📍 Affects 1 file
  • backend/controllers/jobController.js#L108-L109 (this comment)
  • backend/controllers/jobController.js#L137-L141
🤖 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 `@backend/controllers/jobController.js` around lines 108 - 109, Update
backend/controllers/jobController.js lines 108-109 to pass the requested or
session role through normalizeRole and fall back to the default role when
normalization fails; retain the normalized role for cacheKey generation. At
lines 137-141, normalize every stored role with normalizeRole and skip entries
that fail normalization before invoking Adzuna.

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.

[Bug]: Side-effect mutation of Date object in getFlashcardStats query

1 participant