Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8875e16
test: adicionar testes para a página Cursos, incluindo renderização, …
icrcode Jun 9, 2026
41d7670
test: adicionar testes para a página Usuários, incluindo renderização…
icrcode Jun 9, 2026
6610a9e
test: atualizar seleção de instituição no modal de criação de curso
icrcode Jun 9, 2026
ea9f5b6
test: adicionar testes para tratamento de erros nas operações de cria…
icrcode Jun 9, 2026
9bea9e2
test: adicionar testes para tratamento de erros nas operações de cria…
icrcode Jun 9, 2026
c3165f3
test: adicionar testes para tratamento de erros nas operações de cria…
icrcode Jun 9, 2026
ea8bb42
test: adicionar testes para a página Dashboard, incluindo renderizaçã…
icrcode Jun 9, 2026
cf913f8
test: corrigir texto do link na página Dashboard para coordenador
icrcode Jun 9, 2026
88370ff
test: adicionar testes para exibição de erros na alteração de senha e…
icrcode Jun 9, 2026
49ab374
test: adicionar testes para exibição de erros ao atualizar perfil e e…
icrcode Jun 9, 2026
ea2b18a
test: adicionar testes para rejeição de erros sem resposta e filtros …
icrcode Jun 9, 2026
f8616f3
test: adicionar mock de usuário com perfil desconhecido nos testes da…
icrcode Jun 9, 2026
67bae9a
test: adicionar testes para atualização do meta color-scheme e fallba…
icrcode Jun 9, 2026
3acb2de
test: adicionar teste para renderização de Outlet em PrivateRoute sem…
icrcode Jun 9, 2026
066323d
test: atualizar tipos de documentos em MeusCertificados e Verificar p…
icrcode Jun 9, 2026
b905335
test: atualizar tipos de documentos para certificados e ajustar teste…
icrcode Jun 9, 2026
29213ed
test: adicionar pré-visualização de documento PDF e funcionalidade de…
icrcode Jun 9, 2026
e7f9875
test: adicionar pré-visualização de PDF e tratamento de falhas na URL…
icrcode Jun 9, 2026
e381271
test: adicionar tratamento de eventos de teclado e estilo de overflow…
icrcode Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/src/modulos/documentos/rotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ router.post('/', autenticar, exigirPerfil('estudante'), upload.single('arquivo')
return;
}

const TIPOS_VALIDOS = [
'certificado_curso',
'certificado_evento',
'declaracao_participacao',
'comprovante_atividade',
'artigo_publicado',
'outro',
];
if (!TIPOS_VALIDOS.includes(tipo)) {
res.status(400).json({
erro: `Tipo inválido. Valores aceitos: ${TIPOS_VALIDOS.join(', ')}`,
});
return;
}

try {
const usuario = await buscarUsuario(req.usuario!.sub);
if (!usuario?.curso_id) {
Expand Down
10 changes: 6 additions & 4 deletions backend/src/servicos/gerador-certificado.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ export function gerarHash(documentoId: string): string {
}

const TIPO_LEGIVEL: Record<string, string> = {
estagio: 'Estágio',
tcc: 'Trabalho de Conclusão de Curso',
extensao: 'Atividade de Extensão',
monitoria: 'Monitoria',
certificado_curso: 'Certificado de Curso',
certificado_evento: 'Certificado de Evento',
declaracao_participacao: 'Declaração de Participação',
comprovante_atividade: 'Comprovante de Atividade',
artigo_publicado: 'Artigo Publicado',
outro: 'Outro',
};

export async function gerarPDF(dados: DadosCertificado, urlVerificacao: string): Promise<Buffer> {
Expand Down
8 changes: 4 additions & 4 deletions backend/testes/helpers/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import type { Documento } from '../../src/modulos/documentos/repositorio';

export const DOC_MOCK: Documento = {
id: 'doc-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
carga_horaria: 40,
estudante_id: 'estudante-id',
curso_id: 'curso-1',
status: 'pendente',
coordenador_id: null,
nome_arquivo: 'estagio.pdf',
caminho_arquivo: 'documentos/estudante-id/estagio.pdf',
nome_arquivo: 'certificado.pdf',
caminho_arquivo: 'documentos/estudante-id/certificado.pdf',
tamanho_arquivo: 1024,
mime_type: 'application/pdf',
criado_em: new Date(),
Expand Down
34 changes: 17 additions & 17 deletions backend/testes/rotas/documentos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('GET /:id', () => {
const res = await request(app).get('/doc-1');
expect(res.status).toBe(200);
expect(res.body.id).toBe('doc-1');
expect(res.body.titulo).toBe('Estágio XYZ');
expect(res.body.titulo).toBe('Certificado XYZ');
});

it('retorna 404 quando o documento não existe', async () => {
Expand Down Expand Up @@ -106,8 +106,8 @@ describe('POST /', () => {
it('retorna 400 quando nenhum arquivo é enviado', async () => {
const res = await request(app)
.post('/')
.field('titulo', 'Estágio')
.field('tipo', 'estagio')
.field('titulo', 'Certificado')
.field('tipo', 'certificado_curso')
.field('carga_horaria', '40');
expect(res.status).toBe(400);
expect(res.body.erro).toContain('arquivo');
Expand All @@ -132,8 +132,8 @@ describe('POST /', () => {
filename: 'test.pdf',
contentType: 'application/pdf',
})
.field('titulo', 'Estágio')
.field('tipo', 'estagio')
.field('titulo', 'Certificado')
.field('tipo', 'certificado_curso')
.field('carga_horaria', 'abc');
expect(res.status).toBe(400);
expect(res.body.erro).toContain('carga_horaria');
Expand All @@ -147,38 +147,38 @@ describe('POST /', () => {
filename: 'test.pdf',
contentType: 'application/pdf',
})
.field('titulo', 'Estágio XYZ')
.field('tipo', 'estagio')
.field('titulo', 'Certificado XYZ')
.field('tipo', 'certificado_curso')
.field('carga_horaria', '40');
expect(res.status).toBe(422);
expect(res.body.erro).toContain('curso');
});

it('retorna 201 com o documento criado após upload bem-sucedido', async () => {
mockUsr.buscarPorId.mockResolvedValueOnce({ id: 'estudante-id', curso_id: 'curso-1' } as any);
mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/estagio.pdf');
mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/certificado.pdf');
mockDoc.criar.mockResolvedValueOnce(DOC_MOCK);

const res = await request(app)
.post('/')
.attach('arquivo', Buffer.from('%PDF-1.4'), {
filename: 'estagio.pdf',
filename: 'certificado.pdf',
contentType: 'application/pdf',
})
.field('titulo', 'Estágio XYZ')
.field('tipo', 'estagio')
.field('titulo', 'Certificado XYZ')
.field('tipo', 'certificado_curso')
.field('carga_horaria', '40');
expect(res.status).toBe(201);
expect(res.body.id).toBe('doc-1');
expect(res.body.titulo).toBe('Estágio XYZ');
expect(res.body.titulo).toBe('Certificado XYZ');
expect(mockEmitir).toHaveBeenCalledWith(
'documento_submetido',
expect.objectContaining({
documentoId: 'doc-1',
estudanteId: 'estudante-id',
cursoId: 'curso-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
}),
);
});
Expand All @@ -189,9 +189,9 @@ describe('POST /', () => {

await request(app)
.post('/')
.attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'estagio.pdf', contentType: 'application/pdf' })
.field('titulo', 'Estágio XYZ')
.field('tipo', 'estagio')
.attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'certificado.pdf', contentType: 'application/pdf' })
.field('titulo', 'Certificado XYZ')
.field('tipo', 'certificado_curso')
.field('carga_horaria', '40');

expect(mockEmitir).not.toHaveBeenCalled();
Expand Down
2 changes: 1 addition & 1 deletion backend/testes/rotas/publica.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('GET /verificar/:hash', () => {
expect(res.status).toBe(200);
expect(res.body.valido).toBe(true);
expect(res.body.certificado.id).toBe('cert-1');
expect(res.body.documento.titulo).toBe('Estágio XYZ');
expect(res.body.documento.titulo).toBe('Certificado XYZ');
expect(res.body.estudante.nome).toBe('João Silva');
});

Expand Down
6 changes: 3 additions & 3 deletions backend/testes/unitarios/eventos/barramento.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const submetido: EventoDocumentoSubmetido = {
documentoId: 'doc-1',
estudanteId: 'est-1',
cursoId: 'curso-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
};

const aprovado: EventoDocumentoAprovado = {
documentoId: 'doc-1',
estudanteId: 'est-1',
coordenadorId: 'coord-1',
titulo: 'Estágio XYZ',
titulo: 'Certificado XYZ',
};

const reprovado: EventoDocumentoReprovado = {
Expand Down
22 changes: 11 additions & 11 deletions backend/testes/unitarios/eventos/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ const ESTUDANTE_MOCK = {

const DOCUMENTO_MOCK = {
id: 'doc-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
carga_horaria: 40,
estudante_id: 'est-1',
curso_id: 'curso-1',
status: 'aprovado',
coordenador_id: 'coord-1',
nome_arquivo: 'estagio.pdf',
caminho_arquivo: 'documentos/est-1/estagio.pdf',
nome_arquivo: 'certificado.pdf',
caminho_arquivo: 'documentos/est-1/certificado.pdf',
tamanho_arquivo: 1024,
mime_type: 'application/pdf',
criado_em: new Date(),
Expand Down Expand Up @@ -85,8 +85,8 @@ describe('aoDocumentoSubmetido', () => {
documentoId: 'doc-1',
estudanteId: 'est-1',
cursoId: 'curso-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
};

it('notifica cada coordenador encontrado para o curso', async () => {
Expand All @@ -105,7 +105,7 @@ describe('aoDocumentoSubmetido', () => {
destinatarioId: 'coord-1',
destinatarioEmail: 'maria@uni.edu',
assunto: expect.stringContaining('validação'),
mensagem: expect.stringContaining('Estágio XYZ'),
mensagem: expect.stringContaining('Certificado XYZ'),
}),
);
});
Expand Down Expand Up @@ -134,7 +134,7 @@ describe('aoDocumentoAprovado', () => {
documentoId: 'doc-1',
estudanteId: 'est-1',
coordenadorId: 'coord-1',
titulo: 'Estágio XYZ',
titulo: 'Certificado XYZ',
};

it('gera certificado, persiste no banco e notifica o estudante', async () => {
Expand All @@ -161,7 +161,7 @@ describe('aoDocumentoAprovado', () => {
destinatarioId: 'est-1',
destinatarioEmail: 'joao@uni.edu',
assunto: expect.stringContaining('aprovado'),
mensagem: expect.stringContaining('Estágio XYZ'),
mensagem: expect.stringContaining('Certificado XYZ'),
}),
);
});
Expand Down Expand Up @@ -208,7 +208,7 @@ describe('aoDocumentoReprovado', () => {
documentoId: 'doc-1',
estudanteId: 'est-1',
coordenadorId: 'coord-1',
titulo: 'Estágio XYZ',
titulo: 'Certificado XYZ',
observacoes: 'Documentação incompleta',
};

Expand Down Expand Up @@ -250,7 +250,7 @@ describe('aoDocumentoRevisaoSolicitada', () => {
documentoId: 'doc-1',
estudanteId: 'est-1',
coordenadorId: 'coord-1',
titulo: 'Estágio XYZ',
titulo: 'Certificado XYZ',
observacoes: 'Ajustar carga horária',
};

Expand Down
16 changes: 8 additions & 8 deletions backend/testes/unitarios/repositorios/documento.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ const mockQuery = pool.query as jest.Mock;

const DOC_ROW = {
id: 'doc-1',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
carga_horaria: 40,
estudante_id: 'est-1',
curso_id: 'curso-1',
status: 'pendente',
coordenador_id: null,
nome_arquivo: 'estagio.pdf',
caminho_arquivo: 'documentos/est-1/estagio.pdf',
nome_arquivo: 'certificado.pdf',
caminho_arquivo: 'documentos/est-1/certificado.pdf',
tamanho_arquivo: 1024,
mime_type: 'application/pdf',
criado_em: new Date(),
Expand All @@ -39,13 +39,13 @@ describe('criar', () => {
mockQuery.mockResolvedValueOnce({ rows: [DOC_ROW] });

const resultado = await criar({
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
carga_horaria: 40,
estudante_id: 'est-1',
curso_id: 'curso-1',
nome_arquivo: 'estagio.pdf',
caminho_arquivo: 'documentos/est-1/estagio.pdf',
nome_arquivo: 'certificado.pdf',
caminho_arquivo: 'documentos/est-1/certificado.pdf',
tamanho_arquivo: 1024,
mime_type: 'application/pdf',
});
Expand Down
2 changes: 1 addition & 1 deletion backend/testes/unitarios/repositorios/validacao.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const clienteMock = {

const DOC_ROW = {
id: 'doc-1',
titulo: 'Estágio XYZ',
titulo: 'Certificado XYZ',
status: 'pendente',
estudante_id: 'est-1',
curso_id: 'curso-1',
Expand Down
4 changes: 2 additions & 2 deletions backend/testes/unitarios/servicos/gerador-certificado.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const DADOS_BASE: DadosCertificado = {
documentoId: 'doc-1',
estudanteNome: 'João Silva',
estudanteEmail: 'joao@uni.edu',
titulo: 'Estágio XYZ',
tipo: 'estagio',
titulo: 'Certificado XYZ',
tipo: 'certificado_curso',
cargaHoraria: 40,
dataAprovacao: new Date('2024-06-01'),
};
Expand Down
47 changes: 33 additions & 14 deletions frontend/src/pages/Cursos.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { cursosService, type CursoComContagem, type CriarCursoInput } from '../services/cursos';
import { instituicoesService, type Instituicao } from '../services/instituicoes';
Expand Down Expand Up @@ -47,21 +47,40 @@
onClose: () => void;
children: React.ReactNode;
}) {
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);

useEffect(() => {
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [onClose]);

return (
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/40 px-4 py-8">
<div className="w-full max-w-lg rounded-2xl bg-[#011140] shadow-xl">
<div className="flex items-center justify-between border-b border-white/8 px-6 py-4">
<h2 className="text-base font-semibold text-white">{titulo}</h2>
<button
onClick={onClose}
className="rounded-lg p-1.5 text-white/45 hover:bg-white/6 hover:text-white/70"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div
className="fixed inset-0 z-50 overflow-y-auto bg-black/50"
onClick={onClose}
>

Check warning on line 65 in frontend/src/pages/Cursos.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Visible, non-interactive elements with click handlers must have at least one keyboard listener.

See more on https://sonarcloud.io/project/issues?id=icrcode_valida-ai&issues=AZ6uAgj4ArGA7N8BPsHq&open=AZ6uAgj4ArGA7N8BPsHq&pullRequest=58

Check warning on line 65 in frontend/src/pages/Cursos.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.

See more on https://sonarcloud.io/project/issues?id=icrcode_valida-ai&issues=AZ6uAgj4ArGA7N8BPsHp&open=AZ6uAgj4ArGA7N8BPsHp&pullRequest=58
<div className="flex min-h-full items-start justify-center px-4 py-8">
<div
className="w-full max-w-lg rounded-2xl bg-[#011140] shadow-xl"
onClick={(e) => e.stopPropagation()}
>

Check warning on line 70 in frontend/src/pages/Cursos.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.

See more on https://sonarcloud.io/project/issues?id=icrcode_valida-ai&issues=AZ6uAgj4ArGA7N8BPsHr&open=AZ6uAgj4ArGA7N8BPsHr&pullRequest=58

Check warning on line 70 in frontend/src/pages/Cursos.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Visible, non-interactive elements with click handlers must have at least one keyboard listener.

See more on https://sonarcloud.io/project/issues?id=icrcode_valida-ai&issues=AZ6uAgj4ArGA7N8BPsHs&open=AZ6uAgj4ArGA7N8BPsHs&pullRequest=58
<div className="flex items-center justify-between border-b border-white/8 px-6 py-4">
<h2 className="text-base font-semibold text-white">{titulo}</h2>
<button
onClick={onClose}
className="rounded-lg p-1.5 text-white/45 hover:bg-white/6 hover:text-white/70"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="px-6 py-5">{children}</div>
</div>
<div className="px-6 py-5">{children}</div>
</div>
</div>
);
Expand Down
Loading
Loading