diff --git a/backend/src/config.js b/backend/src/config.js index efd70780..bdb13bd2 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -1,5 +1,8 @@ import { existsSync, readFileSync } from "fs"; import path from "path"; +import { fileURLToPath } from "url"; + +const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_KEYWORDS = [ "Java", @@ -35,14 +38,29 @@ function parseNumber(value, fallback) { return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } +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 parseKeywords(value) { // Tenta pegar do arquivo environment.json try { - const envPath = path.resolve(process.cwd(), "src", "db", "environment.json"); + const envPath = getKeywordsFilePath(); if (existsSync(envPath)) { const data = JSON.parse(readFileSync(envPath, "utf-8")); - if (Array.isArray(data.KEYWORDS) && data.KEYWORDS.length > 0) { - return data.KEYWORDS; + if (Array.isArray(data.KEYWORDS)) { + return normalizeKeywords(data.KEYWORDS) ?? []; } } } catch (err) { diff --git a/backend/src/jobsApiApp.js b/backend/src/jobsApiApp.js index 457e6075..9da94af9 100644 --- a/backend/src/jobsApiApp.js +++ b/backend/src/jobsApiApp.js @@ -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))]; +} + +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 parseAllowedOrigins(value) { const configuredOrigins = String(value ?? "") .split(",") @@ -258,27 +297,20 @@ export function createJobsApiApp(options = {}) { */ app.post("/api/keywords", (req, res) => { try { - const { keywords } = req.body; + const normalizedKeywords = normalizeKeywords(req.body?.keywords); - if (!Array.isArray(keywords)) { + if (normalizedKeywords === null) { return res.status(400).json({ message: "O campo 'keywords' deve ser um array de strings.", }); } - const envPath = path.resolve(process.cwd(), "src", "db", "environment.json"); - let envData = { KEYWORDS: [] }; - - if (existsSync(envPath)) { - try { - envData = JSON.parse(readFileSync(envPath, "utf-8")); - } catch (e) { - // Se o arquivo estiver corrompido, reinicia com o objeto padrao - } - } + const envData = { + ...readEnvironmentData(), + KEYWORDS: normalizedKeywords, + }; - envData.KEYWORDS = keywords; - writeFileSync(envPath, JSON.stringify(envData, null, 2), "utf-8"); + writeEnvironmentData(envData); return res.json({ ok: true, @@ -303,28 +335,21 @@ export function createJobsApiApp(options = {}) { * 200: * description: Lista de keywords */ - app.get("/api/keywords", (req, res) => { - try { - const envPath = path.resolve(process.cwd(), "src", "db", "environment.json"); - - if (!existsSync(envPath)) { - return res.json({ keywords: [] }); - } - - const fileContent = readFileSync(envPath, "utf-8"); - const envData = JSON.parse(fileContent); + app.get("/api/keywords", (_req, res) => { + try { + const envData = readEnvironmentData(); - return res.json({ - ok: true, - keywords: envData.KEYWORDS || [], - }); - } catch (error) { - return res.status(500).json({ - message: "Erro ao buscar keywords.", - error: error?.message || "Erro desconhecido", - }); - } -}); + return res.json({ + ok: true, + keywords: normalizeKeywords(envData.KEYWORDS) ?? [], + }); + } catch (error) { + return res.status(500).json({ + message: "Erro ao buscar keywords.", + error: error?.message || "Erro desconhecido", + }); + } + }); /** * @swagger diff --git a/backend/tests/integration/jobsApi.test.js b/backend/tests/integration/jobsApi.test.js index 394378ca..e37867c6 100644 --- a/backend/tests/integration/jobsApi.test.js +++ b/backend/tests/integration/jobsApi.test.js @@ -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: [], + }); + }); + it("GET /api/keywords retorna 200 com as keywords", async () => { const app = createJobsApiApp({ outputDir: tmpDir }); const res = await request(app) diff --git a/backend/tests/unit/utils/config.test.js b/backend/tests/unit/utils/config.test.js index bd839438..3f0f9a79 100644 --- a/backend/tests/unit/utils/config.test.js +++ b/backend/tests/unit/utils/config.test.js @@ -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"]); }); 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([]); + }); });