Skip to content

Commit d81879f

Browse files
committed
feat(tests): add unit tests for services and utilities
- Created unit tests for the app service, cache service, exporter, and logger. - Implemented tests for the pipeline, including request deduplication and scraping from multiple sources. - Added tests for the server entry point and configuration utility. - Introduced TypeScript configuration for the backend.
1 parent 29e2a46 commit d81879f

50 files changed

Lines changed: 870 additions & 120 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
File renamed without changes.

backend/package.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
"name": "backend",
33
"version": "1.0.0",
44
"description": "Backend para scraping e API de vagas do LinkedIn",
5-
"main": "index.js",
5+
"main": "dist/index.js",
66
"type": "module",
77
"scripts": {
8-
"start": "node src/server.js",
9-
"dev": "node src/server.js",
10-
"scraper": "node index.js",
11-
"scraper:watch": "nodemon index.js",
12-
"api": "node src/server.js",
8+
"build": "tsc -p tsconfig.json",
9+
"start": "node dist/src/server.js",
10+
"dev": "tsx src/server.ts",
11+
"scraper": "tsx index.ts",
12+
"scraper:watch": "tsx watch index.ts",
13+
"api": "tsx src/server.ts",
1314
"test": "vitest run",
1415
"test:coverage": "vitest run --coverage",
1516
"test:watch": "vitest --watch",
16-
"validate": "npm test"
17+
"validate": "npm test && npm run build"
1718
},
1819
"keywords": [
1920
"linkedin",
@@ -43,9 +44,15 @@
4344
"node": ">=22.0.0"
4445
},
4546
"devDependencies": {
47+
"@types/cors": "^2.8.19",
48+
"@types/express": "^5.0.5",
49+
"@types/node": "^22.15.35",
50+
"@types/pdfkit": "^0.17.3",
4651
"@vitest/coverage-v8": "^3.2.4",
4752
"nodemon": "^3.1.14",
4853
"supertest": "^7.1.4",
54+
"tsx": "^4.20.6",
55+
"typescript": "^5.8.3",
4956
"vitest": "^3.2.4"
5057
}
5158
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import axios from "axios";
2+
import * as cheerio from "cheerio";
3+
4+
function buildEmpregaCampinasSearchUrl(keyword) {
5+
const url = new URL("https://www.empregacampinas.com.br/");
6+
url.searchParams.set("s", keyword);
7+
return url.toString();
8+
}
9+
10+
function buildEmpregaCampinasRssUrl(keyword) {
11+
const url = new URL("https://www.empregacampinas.com.br/feed/");
12+
url.searchParams.set("s", keyword);
13+
return url.toString();
14+
}
15+
16+
function normalizeText(value) {
17+
return String(value || "").replace(/\s+/g, " ").trim();
18+
}
19+
20+
function parseRssItems(xml, keyword) {
21+
const $ = cheerio.load(xml, { xmlMode: true });
22+
const jobs = [];
23+
24+
$("item").each((_, node) => {
25+
const item = $(node);
26+
const titulo = normalizeText(item.find("title").first().text());
27+
const link = normalizeText(item.find("link").first().text());
28+
const description = normalizeText(item.find("description").first().text());
29+
30+
jobs.push({
31+
source: "Emprega Campinas",
32+
keyword,
33+
titulo,
34+
empresa: "",
35+
local: "Campinas/SP",
36+
link,
37+
descricao: description,
38+
dataPublicacao: normalizeText(item.find("pubDate").first().text()),
39+
});
40+
});
41+
42+
return jobs.filter((job) => job.titulo || job.link);
43+
}
44+
45+
function parseHtmlItems(html, keyword) {
46+
const $ = cheerio.load(html);
47+
const jobs = [];
48+
49+
$("article, .post").each((_, node) => {
50+
const item = $(node);
51+
const link =
52+
item.find("h2 a, .entry-title a").first().attr("href") ||
53+
item.find("a").first().attr("href") ||
54+
"";
55+
56+
const titulo =
57+
normalizeText(item.find("h2 a, .entry-title a").first().text()) ||
58+
normalizeText(item.find("h2, .entry-title").first().text());
59+
60+
jobs.push({
61+
source: "Emprega Campinas",
62+
keyword,
63+
titulo,
64+
empresa: "",
65+
local: "Campinas/SP",
66+
link: normalizeText(link),
67+
});
68+
});
69+
70+
return jobs.filter((job) => job.titulo || job.link);
71+
}
72+
73+
export const empregaCampinasAdapter = {
74+
sourceName: "Emprega Campinas",
75+
76+
async search(keyword, config) {
77+
const timeout = config.pageTimeoutMs || 15000;
78+
const headers = {
79+
"user-agent":
80+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
81+
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
82+
};
83+
84+
try {
85+
const rssUrl = buildEmpregaCampinasRssUrl(keyword);
86+
const rssResponse = await axios.get(rssUrl, { timeout, headers });
87+
const fromRss = parseRssItems(rssResponse.data, keyword);
88+
if (fromRss.length > 0) {
89+
return fromRss;
90+
}
91+
} catch {
92+
// fallback para HTML
93+
}
94+
95+
try {
96+
const htmlUrl = buildEmpregaCampinasSearchUrl(keyword);
97+
const htmlResponse = await axios.get(htmlUrl, { timeout, headers });
98+
return parseHtmlItems(htmlResponse.data, keyword);
99+
} catch {
100+
return [];
101+
}
102+
},
103+
};

backend/src/adapters/glassdoor.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import axios from "axios";
2+
import * as cheerio from "cheerio";
3+
4+
function buildGlassdoorUrl(keyword, location) {
5+
const url = new URL("https://www.glassdoor.com.br/Job/jobs.htm");
6+
url.searchParams.set("sc.keyword", keyword);
7+
if (location) {
8+
url.searchParams.set("locT", "C");
9+
url.searchParams.set("locId", location);
10+
}
11+
return url.toString();
12+
}
13+
14+
function normalizeText(value) {
15+
return String(value || "").replace(/\s+/g, " ").trim();
16+
}
17+
18+
function parseJsonLdJobs(html, keyword) {
19+
const $ = cheerio.load(html);
20+
const jobs = [];
21+
22+
$("script[type='application/ld+json']").each((_, node) => {
23+
const rawJson = $(node).contents().text();
24+
25+
try {
26+
const parsed = JSON.parse(rawJson);
27+
const items = Array.isArray(parsed) ? parsed : [parsed];
28+
29+
for (const item of items) {
30+
if (item?.['@type'] !== "JobPosting") {
31+
continue;
32+
}
33+
34+
jobs.push({
35+
source: "Glassdoor",
36+
keyword,
37+
titulo: normalizeText(item.title),
38+
empresa: normalizeText(item.hiringOrganization?.name),
39+
local: normalizeText(
40+
item.jobLocation?.address?.addressLocality ||
41+
item.jobLocation?.address?.addressRegion,
42+
),
43+
link: normalizeText(item.url),
44+
dataPublicacao: normalizeText(item.datePosted),
45+
});
46+
}
47+
} catch {
48+
// ignora JSON inválido
49+
}
50+
});
51+
52+
return jobs;
53+
}
54+
55+
function parseHtmlJobs(html, keyword) {
56+
const $ = cheerio.load(html);
57+
const jobs = [];
58+
59+
$("li[data-test='jobListing'], article").each((_, node) => {
60+
const item = $(node);
61+
62+
const title = normalizeText(
63+
item.find("a[data-test='job-link'], a.jobLink, a").first().text(),
64+
);
65+
66+
const company = normalizeText(
67+
item.find("[data-test='employer-name'], .EmployerProfile_compactEmployerName__LE242").first().text(),
68+
);
69+
70+
const location = normalizeText(
71+
item.find("[data-test='emp-location'], [data-test='location'], .JobCard_location__Ds1fM").first().text(),
72+
);
73+
74+
const href =
75+
item.find("a[data-test='job-link'], a.jobLink, a").first().attr("href") || "";
76+
77+
const link = href.startsWith("http")
78+
? href
79+
: href
80+
? `https://www.glassdoor.com.br${href}`
81+
: "";
82+
83+
jobs.push({
84+
source: "Glassdoor",
85+
keyword,
86+
titulo: title,
87+
empresa: company,
88+
local: location,
89+
link: normalizeText(link),
90+
});
91+
});
92+
93+
return jobs.filter((job) => job.titulo || job.link);
94+
}
95+
96+
export const glassdoorAdapter = {
97+
sourceName: "Glassdoor",
98+
99+
async search(keyword, config) {
100+
const timeout = config.pageTimeoutMs || 15000;
101+
const url = buildGlassdoorUrl(keyword, config.searchLocation || "");
102+
103+
try {
104+
const response = await axios.get(url, {
105+
timeout,
106+
headers: {
107+
"user-agent":
108+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
109+
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
110+
},
111+
});
112+
113+
const fromJsonLd = parseJsonLdJobs(response.data, keyword);
114+
if (fromJsonLd.length > 0) {
115+
return fromJsonLd;
116+
}
117+
118+
return parseHtmlJobs(response.data, keyword);
119+
} catch {
120+
return [];
121+
}
122+
},
123+
};

0 commit comments

Comments
 (0)