Skip to content

Commit 5a4a0d3

Browse files
authored
Merge pull request #12 from fga-eps-mds/feat/turmas
Feat/turmas
2 parents be63e51 + 0b1c5bf commit 5a4a0d3

9 files changed

Lines changed: 189 additions & 3 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
- Validar o JWT (assinatura/expiração) antes de repassar.
88
- Injetar `X-Internal-Token` (segredo compartilhado) e cabeçalhos auxiliares (`X-User-Id`, `X-User-Papel`, `X-User-Status`) nas chamadas downstream.
9-
- Rotear por path: `/api/v1/autenticacao/*`, `/api/v1/admin/*`, `/api/v1/exemplos/*` → Backend/Auth; `/api/v1/questoes/*` → Quiz-Service; `/api/v1/ia/*` → AI (placeholder enquanto AI estiver vazio).
9+
- Rotear por path: `/api/v1/autenticacao/*`, `/api/v1/admin/*`, `/api/v1/usuarios/*`, `/api/v1/exemplos/*` → Backend/Auth; `/api/v1/questoes/*`, `/api/v1/turmas/*` → Quiz-Service; `/api/v1/ia/*` → AI (placeholder enquanto AI estiver vazio).
1010
- Padronizar respostas de erro vindas do downstream.
1111

1212
## Stack
@@ -106,8 +106,10 @@ make clean # apaga dist/ e coverage/
106106
| `GET /health` | próprio BFF |
107107
| `/api/v1/autenticacao/*` | Backend/Auth `/api/v1/autenticacao/*` |
108108
| `/api/v1/admin/usuarios*` | Backend/Auth `/api/v1/admin/usuarios*` (autenticado) |
109+
| `/api/v1/usuarios/*` | Backend/Auth `/api/v1/usuarios/*` (autenticado) |
109110
| `/api/v1/exemplos/*` | Backend/Auth `/api/v1/exemplos/*` (autenticado) |
110111
| `/api/v1/questoes/*` | Quiz-Service `/api/v1/questoes/*` (autenticado) |
112+
| `/api/v1/turmas/*` | Quiz-Service `/api/v1/turmas/*` (autenticado) |
111113
| `/api/v1/ia/*` | AI `/api/v1/*` — atualmente **503 `IA_INDISPONIVEL`** enquanto `AI_URL` estiver vazio |
112114

113115
### Rotas públicas de autenticação (sem JWT)
@@ -145,6 +147,8 @@ Qualquer outro path de autenticação exige `Authorization: Bearer <accessToken>
145147
│ │ ├── exemplos.routes.ts # exige JWT, repassa Backend
146148
│ │ ├── ia.routes.ts # exige JWT, repassa AI (ou 503 placeholder)
147149
│ │ ├── questoes.routes.ts # exige JWT, repassa Quiz-Service
150+
│ │ ├── turmas.routes.ts # exige JWT, repassa Quiz-Service
151+
│ │ ├── usuarios.routes.ts # exige JWT, repassa Backend
148152
│ │ └── index.ts # monta o apiRouter
149153
│ ├── shared/
150154
│ │ ├── clients/

src/routes/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { authRouter } from "@/routes/auth.routes";
55
import { exemplosRouter } from "@/routes/exemplos.routes";
66
import { iaRouter } from "@/routes/ia.routes";
77
import { questoesRouter } from "@/routes/questoes.routes";
8+
import { turmasRouter } from "@/routes/turmas.routes";
9+
import { usuariosRouter } from "@/routes/usuarios.routes";
810

911
const apiRouter = Router();
1012

@@ -13,6 +15,7 @@ apiRouter.use("/admin", adminRouter);
1315
apiRouter.use("/exemplos", exemplosRouter);
1416
apiRouter.use("/questoes", questoesRouter);
1517
apiRouter.use("/ia", iaRouter);
16-
apiRouter.use("/questoes", questoesRouter);
18+
apiRouter.use("/turmas", turmasRouter);
19+
apiRouter.use("/usuarios", usuariosRouter);
1720

1821
export { apiRouter };

src/routes/turmas.routes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Router } from "express";
2+
import { quizClient } from "@/shared/clients/quiz.client";
3+
import { middlewareAutenticacao } from "@/shared/middlewares/autenticacao.middleware";
4+
import { criarProxyHandler } from "@/shared/middlewares/proxy.middleware";
5+
6+
const router = Router();
7+
8+
router.use(middlewareAutenticacao);
9+
10+
router.all(/.*/, criarProxyHandler(quizClient));
11+
12+
export { router as turmasRouter };

src/routes/usuarios.routes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Router } from "express";
2+
3+
import { backendClient } from "@/shared/clients/backend.client";
4+
import { middlewareAutenticacao } from "@/shared/middlewares/autenticacao.middleware";
5+
import { criarProxyHandler } from "@/shared/middlewares/proxy.middleware";
6+
7+
const router = Router();
8+
9+
router.use(middlewareAutenticacao);
10+
router.all(/.*/, criarProxyHandler(backendClient));
11+
12+
export { router as usuariosRouter };

tests/integration/app.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,35 @@ describe("/api/v1/admin", () => {
177177
});
178178
});
179179

180+
describe("/api/v1/usuarios", () => {
181+
it("rejeita sem token", async () => {
182+
const resposta = await request(aplicacao).get("/api/v1/usuarios/alunos");
183+
184+
expect(resposta.status).toBe(401);
185+
expect(backendMock.request).not.toHaveBeenCalled();
186+
});
187+
188+
it("repassa chamadas autenticadas para o Backend", async () => {
189+
backendMock.request.mockResolvedValue({
190+
status: 200,
191+
data: { mensagem: "ok", dados: [] },
192+
headers: {},
193+
});
194+
195+
const resposta = await request(aplicacao)
196+
.get("/api/v1/usuarios/alunos?busca=joao")
197+
.set("Authorization", `Bearer ${tokenValido()}`);
198+
199+
expect(resposta.status).toBe(200);
200+
expect(quizMock.request).not.toHaveBeenCalled();
201+
const args = backendMock.request.mock.calls[0][0];
202+
expect(args.method).toBe("GET");
203+
expect(args.url).toBe("/api/v1/usuarios/alunos?busca=joao");
204+
expect(args.headers["x-internal-token"]).toBeDefined();
205+
expect(args.headers["x-user-id"]).toBe("u1");
206+
});
207+
});
208+
180209
describe("/api/v1/exemplos", () => {
181210
it("repassa POST autenticado", async () => {
182211
backendMock.request.mockResolvedValue({
@@ -245,4 +274,4 @@ describe("erro do downstream", () => {
245274
.send({ email: "a@b.com" });
246275
expect(resposta.status).toBe(502);
247276
});
248-
});
277+
});

tests/unit/turmas.routes.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import request from 'supertest';
2+
import express from 'express';
3+
import type { Request, Response, NextFunction } from 'express';
4+
5+
jest.mock('@/shared/middlewares/autenticacao.middleware', () => ({
6+
middlewareAutenticacao: jest.fn((req: Request, res: Response, next: NextFunction) => {
7+
next();
8+
}),
9+
}));
10+
11+
jest.mock('@/shared/middlewares/proxy.middleware', () => ({
12+
criarProxyHandler: jest.fn(() => (req: Request, res: Response) => {
13+
res.status(200).json({ mensagem: 'Passou pelo proxy!' });
14+
}),
15+
}));
16+
17+
jest.mock('@/shared/clients/quiz.client', () => ({
18+
quizClient: { dummy: 'mock-client' },
19+
}));
20+
21+
import { turmasRouter } from '@/routes/turmas.routes';
22+
import { middlewareAutenticacao } from '@/shared/middlewares/autenticacao.middleware';
23+
24+
describe('Turmas Router (BFF)', () => {
25+
let app: express.Express;
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
30+
app = express();
31+
app.use('/api/v1/turmas', turmasRouter);
32+
});
33+
34+
35+
it('deve passar pelo middleware de autenticação e chegar no proxy ao fazer um GET', async () => {
36+
const response = await request(app).get('/api/v1/turmas');
37+
38+
expect(middlewareAutenticacao).toHaveBeenCalled();
39+
expect(response.status).toBe(200);
40+
expect(response.body).toEqual({ mensagem: 'Passou pelo proxy!' });
41+
});
42+
43+
it('deve barrar a requisição se o middleware de autenticação falhar', async () => {
44+
(middlewareAutenticacao as jest.Mock).mockImplementationOnce((req: Request, res: Response) => {
45+
res.status(401).json({ erro: 'Não autorizado' });
46+
});
47+
48+
const response = await request(app).get('/api/v1/turmas');
49+
50+
expect(middlewareAutenticacao).toHaveBeenCalled();
51+
expect(response.status).toBe(401);
52+
expect(response.body).toEqual({ erro: 'Não autorizado' });
53+
});
54+
55+
it('deve rotear qualquer método HTTP (POST, PUT, DELETE) e sub-rotas para o proxy', async () => {
56+
57+
const responsePost = await request(app).post('/api/v1/turmas/123/alunos');
58+
expect(responsePost.status).toBe(200);
59+
60+
const responseDelete = await request(app).delete('/api/v1/turmas/999');
61+
expect(responseDelete.status).toBe(200);
62+
});
63+
});

tests/unit/usuarios.routes.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import express from "express";
2+
import type { NextFunction, Request, Response } from "express";
3+
import request from "supertest";
4+
5+
jest.mock("@/shared/middlewares/autenticacao.middleware", () => ({
6+
middlewareAutenticacao: jest.fn((_req: Request, _res: Response, next: NextFunction) => {
7+
next();
8+
}),
9+
}));
10+
11+
jest.mock("@/shared/middlewares/proxy.middleware", () => ({
12+
criarProxyHandler: jest.fn(() => (_req: Request, res: Response) => {
13+
res.status(200).json({ mensagem: "Passou pelo proxy de usuarios!" });
14+
}),
15+
}));
16+
17+
jest.mock("@/shared/clients/backend.client", () => ({
18+
backendClient: { dummy: "mock-client" },
19+
}));
20+
21+
import { usuariosRouter } from "@/routes/usuarios.routes";
22+
import { middlewareAutenticacao } from "@/shared/middlewares/autenticacao.middleware";
23+
24+
describe("Usuarios Router (BFF)", () => {
25+
let app: express.Express;
26+
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
30+
app = express();
31+
app.use("/api/v1/usuarios", usuariosRouter);
32+
});
33+
34+
it("deve passar pelo middleware de autenticacao e chegar no proxy ao fazer um GET", async () => {
35+
const response = await request(app).get("/api/v1/usuarios/alunos?busca=joao");
36+
37+
expect(middlewareAutenticacao).toHaveBeenCalled();
38+
expect(response.status).toBe(200);
39+
expect(response.body).toEqual({ mensagem: "Passou pelo proxy de usuarios!" });
40+
});
41+
42+
it("deve barrar a requisicao se o middleware de autenticacao falhar", async () => {
43+
(middlewareAutenticacao as jest.Mock).mockImplementationOnce(
44+
(_req: Request, res: Response) => {
45+
res.status(401).json({ erro: "Nao autorizado" });
46+
},
47+
);
48+
49+
const response = await request(app).get("/api/v1/usuarios/alunos");
50+
51+
expect(middlewareAutenticacao).toHaveBeenCalled();
52+
expect(response.status).toBe(401);
53+
expect(response.body).toEqual({ erro: "Nao autorizado" });
54+
});
55+
56+
it("deve rotear sub-rotas e query strings para o proxy", async () => {
57+
const responseIds = await request(app).get("/api/v1/usuarios?ids=aluno-1,aluno-2");
58+
const responseAlunos = await request(app).get("/api/v1/usuarios/alunos?busca=maria");
59+
60+
expect(responseIds.status).toBe(200);
61+
expect(responseAlunos.status).toBe(200);
62+
});
63+
});

0 commit comments

Comments
 (0)