Skip to content

Commit ea7fd72

Browse files
authored
PAV-6: Login social LinkedIn (OAuth) (#145)
## Descrição Implementa o login social com LinkedIn (OAuth) no Painel Vagas. No backend, foi adicionado o provider LinkedIn com validação de email obrigatório. No frontend, o botão "Entrar com Facebook" foi substituído pelo botão "Entrar com LinkedIn" nas telas de login e registro, e a função `getLinkedinAuthUrl` foi adicionada ao authService. Testes foram atualizados para cobrir os novos fluxos e manter o threshold de 80% de cobertura. ## Linear link https://linear.app/tatame/issue/PAV-6/login-social-linkedin-oauth ## Como foi testado Testes unitários — Backend: 309/309 passando | Frontend: 149/153 (4 falhas pré-existentes em jobsService.test.ts)
2 parents dd7bcff + 45a7ace commit ea7fd72

15 files changed

Lines changed: 859 additions & 27 deletions

File tree

.claude/skills/abrir-pr-jobs-scraper/skill.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,22 @@ Rode os comandos de verificacao da tabela acima. Se algum falhar, informe e pare
5050

5151
### Passo 3: Verificar commits
5252

53+
> **Importante:** Este projeto usa um modelo de fork. O remote `origin` aponta para o fork do
54+
> desenvolvedor (ex: `jeremiassnts/Jobs_Scraper_Global`) e o remote `upstream` aponta para o
55+
> repositorio principal (`Benevanio/Jobs_Scraper_Global`). O PR sempre deve ser criado do fork
56+
> (origin) para o upstream, usando a flag `--head` do `gh pr create`.
57+
5358
1. Buscar os commits da branch que estao a frente de develop:
5459
```bash
55-
git fetch origin develop
56-
git log origin/develop..HEAD --oneline
60+
git fetch upstream develop
61+
git log upstream/develop..HEAD --oneline
5762
```
5863
2. **Se nao houver commits a frente de develop:**
5964
- Informar: "Esta branch nao tem commits a frente de develop. Nao ha mudancas para abrir PR."
6065
- Parar a execucao
6166
3. **Se ja existir um PR aberto para essa branch:**
62-
- Verificar com: `gh pr list --head $(git branch --show-current) --repo Benevanio/Jobs_Scraper_Global --state open`
67+
- Detectar o owner do fork: `gh repo view --json owner -q .owner.login` (rodado no diretorio do projeto, que aponta para origin)
68+
- Verificar com: `gh pr list --head <owner-do-fork>:$(git branch --show-current) --repo Benevanio/Jobs_Scraper_Global --state open`
6369
- Se existir, informar ao usuario e perguntar se quer atualizar o PR existente ou parar
6470

6571
### Passo 4: Rodar testes
@@ -81,11 +87,11 @@ Rode os comandos de verificacao da tabela acima. Se algum falhar, informe e pare
8187

8288
1. Obter o diff completo contra develop:
8389
```bash
84-
git diff origin/develop..HEAD
90+
git diff upstream/develop..HEAD
8591
```
8692
2. Obter a lista de commits:
8793
```bash
88-
git log origin/develop..HEAD --oneline
94+
git log upstream/develop..HEAD --oneline
8995
```
9096
3. Gerar um **resumo curto em linguagem natural** das mudancas:
9197
- Foco no que foi desenvolvido
@@ -130,27 +136,37 @@ Mostrar tudo formatado e perguntar: **"O PR esta correto? Confirma a criacao? (s
130136

131137
### Passo 8: Criar o PR
132138

133-
1. Verificar se a branch foi enviada ao remote:
139+
> **Modelo de fork:** O PR e criado do fork (origin) para o upstream (Benevanio/Jobs_Scraper_Global).
140+
> E necessario usar `--head <owner-do-fork>:<branch>` para que o GitHub identifique corretamente
141+
> a branch de origem no fork.
142+
143+
1. Verificar se a branch foi enviada ao remote (origin = fork):
134144
```bash
135145
git ls-remote --heads origin $(git branch --show-current)
136146
```
137147
- **Se a branch nao existe no remote:** perguntar ao usuario: "A branch ainda nao foi enviada ao remote. Deseja fazer push agora? (s/n)"
138148
- Se confirmar, rodar: `git push -u origin $(git branch --show-current)`
139149
- Se negar, parar a execucao
140-
2. Criar o PR via GitHub CLI:
150+
2. Detectar o owner do fork:
151+
```bash
152+
FORK_OWNER=$(gh repo view --json owner -q .owner.login)
153+
```
154+
3. Criar o PR via GitHub CLI:
141155
```bash
142156
gh pr create \
143157
--repo Benevanio/Jobs_Scraper_Global \
144158
--base develop \
159+
--head "$FORK_OWNER:$(git branch --show-current)" \
145160
--title "PAV-XX: <titulo>" \
146161
--body "<body completo>"
147162
```
148-
3. Confirmar ao usuario com o link do PR criado
163+
4. Confirmar ao usuario com o link do PR criado
149164

150165
## Erros comuns
151166

152167
- **Nao verificar pre-requisitos** — sempre verificar antes de iniciar
153168
- **Nao confirmar a task com o usuario** — sempre perguntar, mesmo que a branch seja sugestiva
154169
- **Criar PR sem preview** — sempre mostrar preview e aguardar confirmacao
155170
- **Bloquear por causa de testes falhando** — mostrar as falhas, mas deixar o usuario decidir
156-
- **Esquecer de buscar origin/develop atualizado** — sempre rodar `git fetch origin develop` antes de comparar
171+
- **Esquecer de buscar upstream/develop atualizado** — sempre rodar `git fetch upstream develop` antes de comparar
172+
- **Nao usar --head no gh pr create** — como o projeto usa fork, e obrigatorio passar `--head <owner-do-fork>:<branch>` para o PR ser criado corretamente

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
## Git
44

55
- NUNCA faça commits automaticamente. Sempre pergunte ao usuario antes de commitar.
6+
- Mensagens de commit devem ser escritas em português.

backend/src/modules/auth/auth.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export class AuthService {
3535
callbackUrl,
3636
});
3737

38+
if (!profile.email) {
39+
throw new Error("oauth_email_required");
40+
}
41+
3842
const user = await findOrCreateUser({
3943
provider,
4044
profile,

backend/src/modules/auth/providers/linkedin.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
buildAuthorizationUrl,
44
discovery,
55
} from "openid-client";
6-
import { OAuthProfile } from "../../types/auth.types";
6+
import { ExchangeCodeParams, OAuthProfile } from "../../types/auth.types";
77

88
let _config: Awaited<ReturnType<typeof discovery>> | null = null;
99

@@ -32,20 +32,18 @@ export async function getLinkedinAuthUrl(state: string): Promise<string> {
3232
}
3333

3434
export async function exchangeLinkedinCode({
35-
code,
35+
callbackUrl,
3636
state,
37-
}: {
38-
code: string;
39-
state?: string;
40-
}): Promise<OAuthProfile> {
37+
}: ExchangeCodeParams): Promise<OAuthProfile> {
4138
const config = await getConfig();
4239

43-
const tokens = await authorizationCodeGrant(
44-
config,
45-
new URL(
46-
`${process.env.APP_URL}/auth/linkedin/callback?code=${code}&state=${state}`,
47-
),
48-
);
40+
if (!state) {
41+
throw new Error("State ausente");
42+
}
43+
44+
const tokens = await authorizationCodeGrant(config, new URL(callbackUrl), {
45+
expectedState: state,
46+
});
4947

5048
const claims = tokens.claims();
5149

backend/tests/integration/routes/auth.routes.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,33 @@ describe("Integration - Auth Routes", () => {
282282

283283
expect(res.headers.location).toContain("/login?error=oauth_failed");
284284
});
285+
286+
it("redireciona ao frontend em callback valido para linkedin", async () => {
287+
const res = await request(app)
288+
.get(`${BASE}/linkedin/callback?code=li-code-123&state=valid-state-abc123`)
289+
.expect(302);
290+
291+
expect(res.headers.location).toContain("/auth/callback");
292+
expect(mockAuthService.handleCallback).toHaveBeenCalledWith(
293+
expect.objectContaining({
294+
provider: "linkedin",
295+
code: "li-code-123",
296+
state: "valid-state-abc123",
297+
}),
298+
);
299+
});
300+
301+
it("redireciona com erro quando profile nao tem email", async () => {
302+
mockAuthService.handleCallback.mockRejectedValueOnce(
303+
new Error("oauth_email_required"),
304+
);
305+
306+
const res = await request(app)
307+
.get(`${BASE}/linkedin/callback?code=abc123&state=valid-state-abc123`)
308+
.expect(302);
309+
310+
expect(res.headers.location).toContain("/login?error=oauth_failed");
311+
});
285312
});
286313

287314
// ── POST /register ────────────────────────────────────────────────────────

backend/tests/unit/modules/auth/auth.service.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ vi.mock("../../../../src/modules/auth/providers/auth.provider", () => ({
1616
getAuthUrl: mocks.getAuthUrl,
1717
exchangeCode: mocks.exchangeCode,
1818
},
19+
linkedin: {
20+
getAuthUrl: mocks.getAuthUrl,
21+
exchangeCode: mocks.exchangeCode,
22+
},
1923
},
2024
}));
2125

@@ -132,6 +136,19 @@ describe("AuthService", () => {
132136
);
133137
});
134138

139+
it("throws when profile has no email", async () => {
140+
mocks.exchangeCode.mockResolvedValueOnce({
141+
...mockProfile,
142+
email: undefined,
143+
});
144+
145+
await expect(service.handleCallback(validCallbackParams)).rejects.toThrow(
146+
"oauth_email_required",
147+
);
148+
149+
expect(mocks.findOrCreateUser).not.toHaveBeenCalled();
150+
});
151+
135152
it("calls findOrCreateUser with provider and profile", async () => {
136153
mocks.exchangeCode.mockResolvedValueOnce(mockProfile);
137154
mocks.findOrCreateUser.mockResolvedValueOnce(mockUser);

backend/tests/unit/utils/logger.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ describe("logger", () => {
6565
vi.resetModules();
6666
});
6767

68+
it("logInfo works without ctx", async () => {
69+
const spy = vi
70+
.spyOn(process.stdout, "write")
71+
.mockImplementation(() => true);
72+
73+
const { logInfo } = await import("../../../src/logger.ts");
74+
logInfo("no context");
75+
76+
const output = spy.mock.calls.map((c) => String(c[0])).join("");
77+
const parsed = JSON.parse(output);
78+
79+
expect(parsed.msg).toBe("no context");
80+
81+
spy.mockRestore();
82+
});
83+
6884
it("defaults to info level", async () => {
6985
delete process.env.LOG_LEVEL;
7086
vi.resetModules();

0 commit comments

Comments
 (0)