Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR makes backend keyword persistence/configuration more robust by introducing a configurable keywords file path (KEYWORDS_FILE_PATH) and switching default path resolution from process.cwd() to module-relative paths, ensuring consistent behavior across different runtimes (e.g., Electron/Docker/tests).
Changes:
- Add
KEYWORDS_FILE_PATHsupport and default keyword file location based onimport.meta.url(module directory). - Centralize keyword file read/write + keyword normalization in the API endpoints, and adjust
getConfig()keyword parsing to read from the same file. - Update/add unit and integration tests to cover missing keyword file scenarios and “empty keywords” behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| backend/src/jobsApiApp.js | Adds configurable keyword storage path, keyword normalization, and read/write helpers used by /api/keywords. |
| backend/src/config.js | Reads keywords from configurable file path and normalizes file-provided keywords. |
| backend/tests/unit/utils/config.test.js | Updates config keyword parsing tests and adds coverage for empty-list preservation when file exists. |
| backend/tests/integration/jobsApi.test.js | Ensures KEYWORDS_FILE_PATH is cleaned between tests and adds coverage for creating nested keyword file path + empty list behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return null; | ||
| } | ||
|
|
||
| return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))]; |
There was a problem hiding this comment.
normalizeKeywords coerces every array item with String(...), so non-string values (e.g. numbers/objects) are silently accepted even though the 400 message says the field must be an array of strings. Consider validating that every element is a string (and optionally rejecting non-primitive values) before normalizing, or adjust the error contract/message to match the coercion behavior.
| return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))]; | |
| if (!keywords.every((item) => typeof item === "string")) { | |
| return null; | |
| } | |
| return [...new Set(keywords.map((item) => item.trim()).filter(Boolean))]; |
| function getKeywordsFilePath() { | ||
| const configuredPath = process.env.KEYWORDS_FILE_PATH?.trim(); | ||
| return configuredPath | ||
| ? path.resolve(configuredPath) | ||
| : path.resolve(MODULE_DIR, "db", "environment.json"); | ||
| } | ||
|
|
||
| function normalizeKeywords(keywords) { | ||
| if (!Array.isArray(keywords)) { | ||
| return null; | ||
| } | ||
|
|
||
| return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))]; | ||
| } | ||
|
|
||
| function readEnvironmentData() { | ||
| const envPath = getKeywordsFilePath(); | ||
|
|
||
| if (!existsSync(envPath)) { | ||
| return { KEYWORDS: [] }; | ||
| } | ||
|
|
||
| try { | ||
| const data = JSON.parse(readFileSync(envPath, "utf-8")); | ||
| return data && typeof data === "object" ? data : { KEYWORDS: [] }; | ||
| } catch { | ||
| return { KEYWORDS: [] }; | ||
| } | ||
| } | ||
|
|
||
| function writeEnvironmentData(data) { | ||
| const envPath = getKeywordsFilePath(); | ||
| mkdirSync(path.dirname(envPath), { recursive: true }); | ||
| writeFileSync(envPath, JSON.stringify(data, null, 2), "utf-8"); |
There was a problem hiding this comment.
The keyword file path resolution + keyword normalization logic is duplicated here and in backend/src/config.js (and there is additional keyword normalization elsewhere). This increases the risk of behavior drifting (e.g., different handling of invalid file content). Consider extracting a single shared helper (e.g. src/keywordsStorage.js) used by both the API and getConfig().
| function getKeywordsFilePath() { | |
| const configuredPath = process.env.KEYWORDS_FILE_PATH?.trim(); | |
| return configuredPath | |
| ? path.resolve(configuredPath) | |
| : path.resolve(MODULE_DIR, "db", "environment.json"); | |
| } | |
| function normalizeKeywords(keywords) { | |
| if (!Array.isArray(keywords)) { | |
| return null; | |
| } | |
| return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))]; | |
| } | |
| function readEnvironmentData() { | |
| const envPath = getKeywordsFilePath(); | |
| if (!existsSync(envPath)) { | |
| return { KEYWORDS: [] }; | |
| } | |
| try { | |
| const data = JSON.parse(readFileSync(envPath, "utf-8")); | |
| return data && typeof data === "object" ? data : { KEYWORDS: [] }; | |
| } catch { | |
| return { KEYWORDS: [] }; | |
| } | |
| } | |
| function writeEnvironmentData(data) { | |
| const envPath = getKeywordsFilePath(); | |
| mkdirSync(path.dirname(envPath), { recursive: true }); | |
| writeFileSync(envPath, JSON.stringify(data, null, 2), "utf-8"); | |
| function createKeywordsStorage() { | |
| function getFilePath() { | |
| const configuredPath = process.env.KEYWORDS_FILE_PATH?.trim(); | |
| return configuredPath | |
| ? path.resolve(configuredPath) | |
| : path.resolve(MODULE_DIR, "db", "environment.json"); | |
| } | |
| function normalize(keywords) { | |
| if (!Array.isArray(keywords)) { | |
| return null; | |
| } | |
| return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))]; | |
| } | |
| function read() { | |
| const envPath = getFilePath(); | |
| if (!existsSync(envPath)) { | |
| return { KEYWORDS: [] }; | |
| } | |
| try { | |
| const data = JSON.parse(readFileSync(envPath, "utf-8")); | |
| return data && typeof data === "object" ? data : { KEYWORDS: [] }; | |
| } catch { | |
| return { KEYWORDS: [] }; | |
| } | |
| } | |
| function write(data) { | |
| const envPath = getFilePath(); | |
| mkdirSync(path.dirname(envPath), { recursive: true }); | |
| writeFileSync(envPath, JSON.stringify(data, null, 2), "utf-8"); | |
| } | |
| return { | |
| getFilePath, | |
| normalize, | |
| read, | |
| write, | |
| }; | |
| } | |
| const keywordsStorage = createKeywordsStorage(); | |
| function getKeywordsFilePath() { | |
| return keywordsStorage.getFilePath(); | |
| } | |
| function normalizeKeywords(keywords) { | |
| return keywordsStorage.normalize(keywords); | |
| } | |
| function readEnvironmentData() { | |
| return keywordsStorage.read(); | |
| } | |
| function writeEnvironmentData(data) { | |
| keywordsStorage.write(data); |
| it("POST /api/keywords cria o arquivo configurado e aceita lista vazia", async () => { | ||
| tmpDir = mkdtempSync(join(tmpdir(), "jobs-api-")); | ||
| process.env.KEYWORDS_FILE_PATH = join(tmpDir, "nested", "environment.json"); | ||
|
|
||
| const app = createJobsApiApp({ outputDir: tmpDir }); | ||
| const res = await request(app) | ||
| .post("/api/keywords") | ||
| .send({ keywords: [" ", ""] }) | ||
| .expect(200); | ||
|
|
||
| expect(res.body).toEqual({ | ||
| ok: true, | ||
| message: "Keywords atualizadas com sucesso.", | ||
| keywords: [], | ||
| }); | ||
| }); |
There was a problem hiding this comment.
This test claims it "cria o arquivo configurado" but it only asserts on the HTTP response. As written, it would pass even if the file was never created/written. Either add assertions that the file exists (and contains the expected JSON) or rename the test to reflect what is actually being verified.
| it("parseia SEARCH_KEYWORDS em lista", () => { | ||
| vi.stubEnv("SEARCH_KEYWORDS", "Java","Spring","RabbitMQ","Docker"); | ||
| const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-")); | ||
| vi.stubEnv("KEYWORDS_FILE_PATH", path.join(tempDir, "missing-environment.json")); | ||
| vi.stubEnv("SEARCH_KEYWORDS", "Java,Spring,RabbitMQ,Docker"); | ||
|
|
||
| const config = getConfig(); | ||
| expect(config.keywords).toEqual(["Java","Spring","RabbitMQ","Docker"]); | ||
| expect(config.keywords).toEqual(["Java", "Spring", "RabbitMQ", "Docker"]); | ||
| }); |
There was a problem hiding this comment.
These tests create temp directories/files via mkdtempSync/writeFileSync but never remove them, which can leave artifacts in the OS temp directory across repeated runs. Consider tracking created paths and cleaning them up in afterEach (e.g., rmSync(tempDir, { recursive: true, force: true })).
| if (Array.isArray(data.KEYWORDS)) { | ||
| return normalizeKeywords(data.KEYWORDS) ?? []; | ||
| } |
There was a problem hiding this comment.
parseKeywords only uses the file when data.KEYWORDS is an array; if the file exists but KEYWORDS has an invalid type, it falls back to env/defaults. Meanwhile, the API's GET /api/keywords will return an empty list for the same invalid content. Consider aligning the behavior (e.g., treat "file exists but KEYWORDS invalid" as an empty list everywhere, or reuse a shared read/normalize helper).
| if (Array.isArray(data.KEYWORDS)) { | |
| return normalizeKeywords(data.KEYWORDS) ?? []; | |
| } | |
| return normalizeKeywords(data?.KEYWORDS) ?? []; |
No description provided.