Repository navigation
Develop #73
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
Develop #73
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,18 +2,57 @@ import cors from "cors"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import express from "express"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { fileURLToPath } from "url"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import XLSX from "xlsx"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { run as runScraper } from "./app.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getConfig } from "./config.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { searchJobsWithCache } from "./pipeline/searchJobsWithCache.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { sources } from "./sources/index.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_ALLOWED_ORIGINS = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "https://painel-vagas-lake.vercel.app", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost:5173", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost:5174", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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))]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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))]; |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,9 +21,11 @@ describe("jobs API", () => { | |
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mocks.run.mockResolvedValue(undefined); | ||
| delete process.env.KEYWORDS_FILE_PATH; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| delete process.env.KEYWORDS_FILE_PATH; | ||
| tmpDir = undefined; | ||
| }); | ||
|
|
||
|
|
@@ -179,6 +181,23 @@ describe("jobs API", () => { | |
| }); | ||
| }); | ||
|
|
||
| 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: [], | ||
| }); | ||
| }); | ||
|
Comment on lines
+184
to
+199
|
||
|
|
||
| it("GET /api/keywords retorna 200 com as keywords", async () => { | ||
| const app = createJobsApiApp({ outputDir: tmpDir }); | ||
| const res = await request(app) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| import { mkdtempSync, writeFileSync } from "fs"; | ||
| import { tmpdir } from "os"; | ||
| import path from "path"; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { getConfig } from "../../../src/config.js"; | ||
|
|
||
|
|
@@ -17,6 +20,7 @@ const CONFIG_ENV_KEYS = [ | |
| "JOB_TYPES", | ||
| "TIME_FILTER", | ||
| "SEARCH_KEYWORDS", | ||
| "KEYWORDS_FILE_PATH", | ||
| ]; | ||
|
|
||
| describe("getConfig", () => { | ||
|
|
@@ -45,9 +49,12 @@ describe("getConfig", () => { | |
| }); | ||
|
|
||
| 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"]); | ||
| }); | ||
|
Comment on lines
51
to
58
|
||
|
|
||
| it("rejeita TIME_FILTER invalido e usa fallback", () => { | ||
|
|
@@ -98,9 +105,24 @@ describe("getConfig", () => { | |
| }); | ||
|
|
||
| it("retorna keywords padrao quando lista informada e vazia", () => { | ||
| const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-")); | ||
| vi.stubEnv("KEYWORDS_FILE_PATH", path.join(tempDir, "missing-environment.json")); | ||
| vi.stubEnv("SEARCH_KEYWORDS", " , , "); | ||
|
|
||
| const config = getConfig(); | ||
| expect(config.keywords.length).toBeGreaterThan(0); | ||
| expect(config.keywords).toContain("Java"); | ||
| }); | ||
|
|
||
| it("preserva lista vazia quando o arquivo de keywords ja existe", () => { | ||
| const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-")); | ||
| const keywordsFile = path.join(tempDir, "environment.json"); | ||
| writeFileSync(keywordsFile, JSON.stringify({ KEYWORDS: [] }), "utf-8"); | ||
|
|
||
| vi.stubEnv("KEYWORDS_FILE_PATH", keywordsFile); | ||
| vi.stubEnv("SEARCH_KEYWORDS", "Java,Node"); | ||
|
|
||
| const config = getConfig(); | ||
| expect(config.keywords).toEqual([]); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseKeywordsonly uses the file whendata.KEYWORDSis an array; if the file exists butKEYWORDShas an invalid type, it falls back to env/defaults. Meanwhile, the API'sGET /api/keywordswill 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).