Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion backend/controllers/jobController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@
const ADZUNA_COUNTRY = process.env.ADZUNA_COUNTRY || "in";
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;

// Valid country codes for Adzuna API (Issue #1301)
const VALID_ADZUNA_COUNTRIES = [
"au", // Australia
"at", // Austria
"be", // Belgium
"br", // Brazil
"ca", // Canada
"de", // Germany
"es", // Spain
"fr", // France
"gb", // United Kingdom
"in", // India
"it", // Italy
"mx", // Mexico
Comment on lines +16 to +23
"nl", // Netherlands
"nz", // New Zealand
"pl", // Poland
"ru", // Russia
"sg", // Singapore
"us", // United States
"za", // South Africa
];

const isValidCountry = (country) => {
return VALID_ADZUNA_COUNTRIES.includes(country.toLowerCase());
};
Comment on lines +33 to +35

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- controller excerpt ---'
cat -n backend/controllers/jobController.js | sed -n '1,120p'

printf '%s\n' '--- Express dependency/config references ---'
rg -n '"express"|express\(|query parser|queryParser|req\.query|isValidCountry|VALID_ADZUNA_COUNTRIES' \
  package.json package-lock.json yarn.lock pnpm-lock.yaml backend 2>/dev/null | head -250

printf '%s\n' '--- relevant tracked files ---'
git ls-files | rg '(^|/)(package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|jobController\.js|app|server|routes?)'

Repository: Canopus-Labs/PrepPilot

Length of output: 8341


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- server configuration ---'
cat -n backend/server.js | sed -n '1,100p'

printf '%s\n' '--- job routes ---'
cat -n backend/routes/jobRoutes.js | sed -n '1,100p'

printf '%s\n' '--- parser configuration and job endpoint tests ---'
rg -n -i 'query\s*parser|set\(["'\'']query|job|country' backend --glob '!package-lock.json' | head -250

printf '%s\n' '--- standalone Node querystring behavior ---'
node - <<'JS'
const querystring = require('node:querystring');

for (const input of [
  'country=us',
  'country=us&country=ca',
  'country[]=us&country[]=ca',
  'country=',
]) {
  const parsed = querystring.parse(input);
  const country = parsed.country;
  let result;
  try {
    result = {
      parsed,
      typeofCountry: typeof country,
      isArray: Array.isArray(country),
      lowercased: country.toLowerCase(),
    };
  } catch (error) {
    result = {
      parsed,
      typeofCountry: typeof country,
      isArray: Array.isArray(country),
      error: error.message,
    };
  }
  console.log(input, JSON.stringify(result));
}
JS

Repository: Canopus-Labs/PrepPilot

Length of output: 10803


🌐 Web query:

Express 5.1 application query parser default simple repeated query parameters arrays official documentation

💡 Result:

In Express 5.1, the default setting for the query parser application setting is 'simple' [1][2]. This parser uses the Node.js built-in querystring module rather than the qs library that was used as the default in previous versions [1][2]. Because of this change, the 'simple' parser does not automatically parse repeated query parameters or bracketed parameters (e.g., items[]) into arrays [3][4]. If you require array parsing for repeated query parameters, you must change the query parser setting [1]. To restore behavior similar to Express 4 (where qs handles array parsing), you can set the parser to 'extended' [4][2]: app.set('query parser', 'extended'); The 'extended' parser uses the qs library, which automatically parses repeated query parameters and bracketed syntax into arrays [4][5]. Note that when using the 'extended' parser, there is a default arrayLimit of 1000, which ensures that repeated keys are correctly returned as arrays rather than objects for up to 1000 items [6][5]. If you need further customization (such as specific qs options), you can also provide a custom function to the query parser setting [7]: const qs = require('qs'); app.set('query parser', (str) => qs.parse(str, { /* custom options */ }));

Citations:


Reject non-string country values before lowercasing.

With Express 5.1’s default parser, ?country=us&country=ca produces an array. .toLowerCase() then throws, and getJobs returns HTTP 500 instead of HTTP 400. Add a typeof country === "string" guard and a regression test.

🤖 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 33 - 35, The
isValidCountry helper currently lowercases non-string values and can throw for
array query parameters. Add a typeof country === "string" guard before calling
toLowerCase, preserving validation for valid strings, and add a regression test
through getJobs that verifies repeated country parameters return HTTP 400 rather
than 500.


// The Jobs feature is optional: without Adzuna credentials it stays dormant
// instead of crashing the server or spamming failed API calls.
const isAdzunaConfigured = () => Boolean(ADZUNA_APP_ID && ADZUNA_API_KEY);
Expand Down Expand Up @@ -53,7 +80,16 @@
.select("role");

const role = req.query.role || latestSession?.role || "software engineer";
const country = req.query.country || ADZUNA_COUNTRY;
const countryParam = req.query.country || ADZUNA_COUNTRY;

// Validate country parameter (Issue #1301)
if (!isValidCountry(countryParam)) {
return res.status(400).json({
message: `Invalid country code. Valid codes are: ${VALID_ADZUNA_COUNTRIES.join(", ")}`,
});
}
Comment on lines +83 to +90

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

Validate country before configuration and database work.

The validation block runs after the disabled-configuration response and after Session.findOne(). An unsupported country therefore receives HTTP 200 when Adzuna is disabled, and it can receive HTTP 500 if the session lookup fails. This conflicts with the stated contract that invalid codes return HTTP 400.

Move country extraction and validation before both operations.

🤖 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 83 - 90, Move countryParam
extraction and the isValidCountry validation block in the job controller to the
start of the request flow, before the disabled-configuration response and before
Session.findOne(). Preserve the existing HTTP 400 response and valid-country
behavior.


const country = countryParam.toLowerCase();
Comment on lines +83 to +92

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 | 🟡 Minor | ⚡ Quick win

Use the normalized default country in refreshJobCache.

If ADZUNA_COUNTRY=US, getJobs uses a role|us cache key, but refreshJobCache at Lines 123-124 uses role|US. The scheduled refresh then writes a cache entry that getJobs does not read. Normalize the default once and pass that value to both the cache key and fetchFromAdzuna().

🤖 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 83 - 92, Update
refreshJobCache to normalize the resolved country once, then reuse that
lowercase value for both the cache key and fetchFromAdzuna(). Ensure the default
ADZUNA_COUNTRY follows the same normalization as the country used by getJobs,
including when it is configured as uppercase.

const cacheKey = `${role.toLowerCase()}|${country}`;

const cached = await JobCache.findOne({ cacheKey });
Expand Down
Loading