Skip to content

Commit 63326a4

Browse files
authored
Develop (#73)
2 parents 4d00d0b + 9071ee2 commit 63326a4

4 files changed

Lines changed: 124 additions & 40 deletions

File tree

backend/src/config.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { existsSync, readFileSync } from "fs";
22
import path from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
36

47
const DEFAULT_KEYWORDS = [
58
"Java",
@@ -35,14 +38,29 @@ function parseNumber(value, fallback) {
3538
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3639
}
3740

41+
function getKeywordsFilePath() {
42+
const configuredPath = process.env.KEYWORDS_FILE_PATH?.trim();
43+
return configuredPath
44+
? path.resolve(configuredPath)
45+
: path.resolve(MODULE_DIR, "db", "environment.json");
46+
}
47+
48+
function normalizeKeywords(keywords) {
49+
if (!Array.isArray(keywords)) {
50+
return null;
51+
}
52+
53+
return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))];
54+
}
55+
3856
function parseKeywords(value) {
3957
// Tenta pegar do arquivo environment.json
4058
try {
41-
const envPath = path.resolve(process.cwd(), "src", "db", "environment.json");
59+
const envPath = getKeywordsFilePath();
4260
if (existsSync(envPath)) {
4361
const data = JSON.parse(readFileSync(envPath, "utf-8"));
44-
if (Array.isArray(data.KEYWORDS) && data.KEYWORDS.length > 0) {
45-
return data.KEYWORDS;
62+
if (Array.isArray(data.KEYWORDS)) {
63+
return normalizeKeywords(data.KEYWORDS) ?? [];
4664
}
4765
}
4866
} catch (err) {

backend/src/jobsApiApp.js

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,57 @@ import cors from "cors";
22
import express from "express";
33
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
44
import path from "path";
5+
import { fileURLToPath } from "url";
56
import XLSX from "xlsx";
67
import { run as runScraper } from "./app.js";
78
import { getConfig } from "./config.js";
89
import { searchJobsWithCache } from "./pipeline/searchJobsWithCache.js";
910
import { sources } from "./sources/index.js";
1011

12+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
13+
1114
const DEFAULT_ALLOWED_ORIGINS = [
1215
"https://painel-vagas-lake.vercel.app",
1316
"http://localhost:5173",
1417
"http://localhost:5174",
1518
];
1619

20+
function getKeywordsFilePath() {
21+
const configuredPath = process.env.KEYWORDS_FILE_PATH?.trim();
22+
return configuredPath
23+
? path.resolve(configuredPath)
24+
: path.resolve(MODULE_DIR, "db", "environment.json");
25+
}
26+
27+
function normalizeKeywords(keywords) {
28+
if (!Array.isArray(keywords)) {
29+
return null;
30+
}
31+
32+
return [...new Set(keywords.map((item) => String(item ?? "").trim()).filter(Boolean))];
33+
}
34+
35+
function readEnvironmentData() {
36+
const envPath = getKeywordsFilePath();
37+
38+
if (!existsSync(envPath)) {
39+
return { KEYWORDS: [] };
40+
}
41+
42+
try {
43+
const data = JSON.parse(readFileSync(envPath, "utf-8"));
44+
return data && typeof data === "object" ? data : { KEYWORDS: [] };
45+
} catch {
46+
return { KEYWORDS: [] };
47+
}
48+
}
49+
50+
function writeEnvironmentData(data) {
51+
const envPath = getKeywordsFilePath();
52+
mkdirSync(path.dirname(envPath), { recursive: true });
53+
writeFileSync(envPath, JSON.stringify(data, null, 2), "utf-8");
54+
}
55+
1756
function parseAllowedOrigins(value) {
1857
const configuredOrigins = String(value ?? "")
1958
.split(",")
@@ -258,27 +297,20 @@ export function createJobsApiApp(options = {}) {
258297
*/
259298
app.post("/api/keywords", (req, res) => {
260299
try {
261-
const { keywords } = req.body;
300+
const normalizedKeywords = normalizeKeywords(req.body?.keywords);
262301

263-
if (!Array.isArray(keywords)) {
302+
if (normalizedKeywords === null) {
264303
return res.status(400).json({
265304
message: "O campo 'keywords' deve ser um array de strings.",
266305
});
267306
}
268307

269-
const envPath = path.resolve(process.cwd(), "src", "db", "environment.json");
270-
let envData = { KEYWORDS: [] };
271-
272-
if (existsSync(envPath)) {
273-
try {
274-
envData = JSON.parse(readFileSync(envPath, "utf-8"));
275-
} catch (e) {
276-
// Se o arquivo estiver corrompido, reinicia com o objeto padrao
277-
}
278-
}
308+
const envData = {
309+
...readEnvironmentData(),
310+
KEYWORDS: normalizedKeywords,
311+
};
279312

280-
envData.KEYWORDS = keywords;
281-
writeFileSync(envPath, JSON.stringify(envData, null, 2), "utf-8");
313+
writeEnvironmentData(envData);
282314

283315
return res.json({
284316
ok: true,
@@ -303,28 +335,21 @@ export function createJobsApiApp(options = {}) {
303335
* 200:
304336
* description: Lista de keywords
305337
*/
306-
app.get("/api/keywords", (req, res) => {
307-
try {
308-
const envPath = path.resolve(process.cwd(), "src", "db", "environment.json");
309-
310-
if (!existsSync(envPath)) {
311-
return res.json({ keywords: [] });
312-
}
313-
314-
const fileContent = readFileSync(envPath, "utf-8");
315-
const envData = JSON.parse(fileContent);
338+
app.get("/api/keywords", (_req, res) => {
339+
try {
340+
const envData = readEnvironmentData();
316341

317-
return res.json({
318-
ok: true,
319-
keywords: envData.KEYWORDS || [],
320-
});
321-
} catch (error) {
322-
return res.status(500).json({
323-
message: "Erro ao buscar keywords.",
324-
error: error?.message || "Erro desconhecido",
325-
});
326-
}
327-
});
342+
return res.json({
343+
ok: true,
344+
keywords: normalizeKeywords(envData.KEYWORDS) ?? [],
345+
});
346+
} catch (error) {
347+
return res.status(500).json({
348+
message: "Erro ao buscar keywords.",
349+
error: error?.message || "Erro desconhecido",
350+
});
351+
}
352+
});
328353

329354
/**
330355
* @swagger

backend/tests/integration/jobsApi.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ describe("jobs API", () => {
2121
beforeEach(() => {
2222
vi.clearAllMocks();
2323
mocks.run.mockResolvedValue(undefined);
24+
delete process.env.KEYWORDS_FILE_PATH;
2425
});
2526

2627
afterEach(() => {
28+
delete process.env.KEYWORDS_FILE_PATH;
2729
tmpDir = undefined;
2830
});
2931

@@ -179,6 +181,23 @@ describe("jobs API", () => {
179181
});
180182
});
181183

184+
it("POST /api/keywords cria o arquivo configurado e aceita lista vazia", async () => {
185+
tmpDir = mkdtempSync(join(tmpdir(), "jobs-api-"));
186+
process.env.KEYWORDS_FILE_PATH = join(tmpDir, "nested", "environment.json");
187+
188+
const app = createJobsApiApp({ outputDir: tmpDir });
189+
const res = await request(app)
190+
.post("/api/keywords")
191+
.send({ keywords: [" ", ""] })
192+
.expect(200);
193+
194+
expect(res.body).toEqual({
195+
ok: true,
196+
message: "Keywords atualizadas com sucesso.",
197+
keywords: [],
198+
});
199+
});
200+
182201
it("GET /api/keywords retorna 200 com as keywords", async () => {
183202
const app = createJobsApiApp({ outputDir: tmpDir });
184203
const res = await request(app)

backend/tests/unit/utils/config.test.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { mkdtempSync, writeFileSync } from "fs";
2+
import { tmpdir } from "os";
3+
import path from "path";
14
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
import { getConfig } from "../../../src/config.js";
36

@@ -17,6 +20,7 @@ const CONFIG_ENV_KEYS = [
1720
"JOB_TYPES",
1821
"TIME_FILTER",
1922
"SEARCH_KEYWORDS",
23+
"KEYWORDS_FILE_PATH",
2024
];
2125

2226
describe("getConfig", () => {
@@ -45,9 +49,12 @@ describe("getConfig", () => {
4549
});
4650

4751
it("parseia SEARCH_KEYWORDS em lista", () => {
48-
vi.stubEnv("SEARCH_KEYWORDS", "Java","Spring","RabbitMQ","Docker");
52+
const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-"));
53+
vi.stubEnv("KEYWORDS_FILE_PATH", path.join(tempDir, "missing-environment.json"));
54+
vi.stubEnv("SEARCH_KEYWORDS", "Java,Spring,RabbitMQ,Docker");
55+
4956
const config = getConfig();
50-
expect(config.keywords).toEqual(["Java","Spring","RabbitMQ","Docker"]);
57+
expect(config.keywords).toEqual(["Java", "Spring", "RabbitMQ", "Docker"]);
5158
});
5259

5360
it("rejeita TIME_FILTER invalido e usa fallback", () => {
@@ -98,9 +105,24 @@ describe("getConfig", () => {
98105
});
99106

100107
it("retorna keywords padrao quando lista informada e vazia", () => {
108+
const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-"));
109+
vi.stubEnv("KEYWORDS_FILE_PATH", path.join(tempDir, "missing-environment.json"));
101110
vi.stubEnv("SEARCH_KEYWORDS", " , , ");
111+
102112
const config = getConfig();
103113
expect(config.keywords.length).toBeGreaterThan(0);
104114
expect(config.keywords).toContain("Java");
105115
});
116+
117+
it("preserva lista vazia quando o arquivo de keywords ja existe", () => {
118+
const tempDir = mkdtempSync(path.join(tmpdir(), "jobs-config-"));
119+
const keywordsFile = path.join(tempDir, "environment.json");
120+
writeFileSync(keywordsFile, JSON.stringify({ KEYWORDS: [] }), "utf-8");
121+
122+
vi.stubEnv("KEYWORDS_FILE_PATH", keywordsFile);
123+
vi.stubEnv("SEARCH_KEYWORDS", "Java,Node");
124+
125+
const config = getConfig();
126+
expect(config.keywords).toEqual([]);
127+
});
106128
});

0 commit comments

Comments
 (0)