From 2688918d093acf68e3c4efc2d3369c38f5d621ec Mon Sep 17 00:00:00 2001 From: jambis-prg Date: Tue, 9 Dec 2025 12:21:30 -0300 Subject: [PATCH 1/3] fix(EspecificacaoDoCalculoDaMedia.ts): throw an error if pesoDoConceito or pesoDaMeta are undefined --- .../src/models/EspecificacaoDoCalculoDaMedia.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/models/EspecificacaoDoCalculoDaMedia.ts b/server/src/models/EspecificacaoDoCalculoDaMedia.ts index 7abcab51..4356739c 100644 --- a/server/src/models/EspecificacaoDoCalculoDaMedia.ts +++ b/server/src/models/EspecificacaoDoCalculoDaMedia.ts @@ -1,5 +1,5 @@ export type Grade = 'MANA' | 'MPA' | 'MA'; -type Meta = string; +export type Meta = string; export class EspecificacaoDoCalculoDaMedia { private readonly pesosDosConceitos: Map; // MA, MPA, MANA @@ -30,8 +30,17 @@ export class EspecificacaoDoCalculoDaMedia { for (const [meta, conceito] of notasDasMetas.entries()) { - const pesoDoConceito = this.pesosDosConceitos.get(conceito)!; - const pesoDaMeta = this.pesosDasMetas.get(meta)!; + // Verifica se o conceito existe + const pesoDoConceito = this.pesosDosConceitos.get(conceito); + if (pesoDoConceito === undefined) { + throw new Error(`Conceito '${conceito}' não encontrado nos pesos dos conceitos.`); + } + + // Verifica se a meta existe + const pesoDaMeta = this.pesosDasMetas.get(meta); + if (pesoDaMeta === undefined) { + throw new Error(`Meta '${meta}' não encontrada nos pesos das metas.`); + } somaTotal += pesoDaMeta * pesoDoConceito; } From eed511105c88903e7062a8ce24c78f342bbf4e7b Mon Sep 17 00:00:00 2001 From: jambis-prg Date: Tue, 9 Dec 2025 12:23:01 -0300 Subject: [PATCH 2/3] test(EspecificacaoDoCalculoDaMedia): class tests implemented and working --- .../especificacao_calculo_media.test.ts | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 server/src/__tests__/especificacao_calculo_media.test.ts diff --git a/server/src/__tests__/especificacao_calculo_media.test.ts b/server/src/__tests__/especificacao_calculo_media.test.ts new file mode 100644 index 00000000..80e8bd22 --- /dev/null +++ b/server/src/__tests__/especificacao_calculo_media.test.ts @@ -0,0 +1,290 @@ +// especificacao_calculo_media.test.ts +import { EspecificacaoDoCalculoDaMedia, Grade, Meta, DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA } from '../models/EspecificacaoDoCalculoDaMedia'; + +describe('EspecificacaoDoCalculoDaMedia', () => { + // Teste 1: Criação da classe com dados válidos + it('deve criar uma instância com pesos válidos', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Gerência de Configuração', 1], + ['Gerência de Projeto', 2], + ['Qualidade de Software', 3], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + expect(especificacao).toBeInstanceOf(EspecificacaoDoCalculoDaMedia); + }); + + // Teste 2: Exceção quando soma dos pesos das metas é zero + it('deve lançar erro quando soma dos pesos das metas é zero', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Meta 1', 0], + ['Meta 2', 0], + ['Meta 3', 0], + ]); + + expect(() => { + new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + }).toThrow('A soma dos pesos das metas não pode ser zero.'); + }); + + // Teste 3: Cálculo de média com todos os conceitos MA + it('deve calcular média 10 quando todas as notas são MA', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Gerência de Configuração', 2], + ['Gerência de Projeto', 3], + ['Qualidade de Software', 1], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + const notasAluno = new Map([ + ['Gerência de Configuração', 'MA'], + ['Gerência de Projeto', 'MA'], + ['Qualidade de Software', 'MA'], + ]); + + const media = especificacao.calc(notasAluno); + expect(media).toBe(10); + }); + + // Teste 4: Cálculo de média com conceitos variados + it('deve calcular média ponderada corretamente com conceitos diferentes', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Gerência de Configuração', 2], + ['Gerência de Projeto', 3], + ['Qualidade de Software', 1], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + const notasAluno = new Map([ + ['Gerência de Configuração', 'MA'], // 2 * 10 = 20 + ['Gerência de Projeto', 'MPA'], // 3 * 7 = 21 + ['Qualidade de Software', 'MANA'], // 1 * 0 = 0 + ]); + + const media = especificacao.calc(notasAluno); + // (20 + 21 + 0) / (2 + 3 + 1) = 41 / 6 = 6.8333... + expect(media).toBeCloseTo(6.8333, 4); + }); + + // Teste 5: Cálculo com pesos de conceitos diferentes + it('deve calcular corretamente com pesos de conceitos customizados', () => { + const pesosConceitos = new Map([ + ['MA', 9], + ['MPA', 6], + ['MANA', 3], + ]); + + const pesosMetas = new Map([ + ['Meta A', 4], + ['Meta B', 6], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + const notasAluno = new Map([ + ['Meta A', 'MA'], // 4 * 9 = 36 + ['Meta B', 'MPA'], // 6 * 6 = 36 + ]); + + const media = especificacao.calc(notasAluno); + // (36 + 36) / (4 + 6) = 72 / 10 = 7.2 + expect(media).toBe(7.2); + }); + + // Teste 6: Serialização e desserialização + it('deve serializar e desserializar corretamente', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Gerência de Configuração', 1], + ['Gerência de Projeto', 2], + ['Qualidade de Software', 3], + ]); + + const original = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + const json = original.toJSON(); + + const reconstruida = EspecificacaoDoCalculoDaMedia.fromJSON(json); + const jsonReconstruida = reconstruida.toJSON(); + + expect(jsonReconstruida).toEqual(json); + + // Testar se o cálculo funciona igual + const notas = new Map([ + ['Gerência de Configuração', 'MA'], + ['Gerência de Projeto', 'MPA'], + ['Qualidade de Software', 'MANA'], + ]); + + expect(reconstruida.calc(notas)).toBe(original.calc(notas)); + }); + + // Teste 7: Desserialização com formato antigo (array de pares) + it('deve desserializar formato antigo (array de pares)', () => { + const jsonAntigo = { + pesosDosConceitos: [['MA', 10], ['MPA', 7], ['MANA', 0]], + pesosDasMetas: [['Gerência de Configuração', 1], ['Gerência de Projeto', 2]] + }; + + const especificacao = EspecificacaoDoCalculoDaMedia.fromJSON(jsonAntigo); + const json = especificacao.toJSON(); + + expect(json.pesosDosConceitos).toEqual({ + MA: 10, + MPA: 7, + MANA: 0 + }); + + expect(json.pesosDasMetas).toEqual({ + 'Gerência de Configuração': 1, + 'Gerência de Projeto': 2 + }); + }); + + // Teste 8: Desserialização com formato de array de objetos + it('deve desserializar formato com array de objetos', () => { + const jsonAntigo = { + pesosDosConceitos: [ + { key: 'MA', value: 10 }, + { key: 'MPA', value: 7 }, + { key: 'MANA', value: 0 } + ], + pesosDasMetas: [ + { key: 'Gerência de Configuração', value: 1 }, + { key: 'Gerência de Projeto', value: 2 } + ] + }; + + const especificacao = EspecificacaoDoCalculoDaMedia.fromJSON(jsonAntigo); + const json = especificacao.toJSON(); + + expect(json.pesosDosConceitos.MA).toBe(10); + expect(json.pesosDasMetas['Gerência de Projeto']).toBe(2); + }); + + // Teste 9: Especificação padrão + it('deve usar a especificação padrão corretamente', () => { + const especificacaoPadrao = DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA; + const json = especificacaoPadrao.toJSON(); + + expect(json.pesosDosConceitos).toEqual({ + MA: 10, + MPA: 7, + MANA: 0 + }); + + expect(json.pesosDasMetas).toEqual({ + 'Gerência de Configuração': 1, + 'Gerência de Projeto': 1, + 'Qualidade de Software': 1 + }); + + // Testar cálculo com especificação padrão + const notas = new Map([ + ['Gerência de Configuração', 'MA'], + ['Gerência de Projeto', 'MPA'], + ['Qualidade de Software', 'MA'], + ]); + + const media = especificacaoPadrao.calc(notas); + // (1*10 + 1*7 + 1*10) / 3 = 27 / 3 = 9 + expect(media).toBe(9); + }); + + // Teste 10: Cálculo com meta não existente deve lançar erro + it('deve lançar erro ao calcular com meta não existente', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Meta Existente', 2], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + const notasAluno = new Map([ + ['Meta Existente', 'MA'], + ['Meta Não Existente', 'MPA'], // Esta meta não tem peso definido + ]); + + // O método get com ! vai lançar erro se for undefined + expect(() => especificacao.calc(notasAluno)).toThrow(); + }); + + // Teste 11: Verificar imutabilidade após criação + it('deve manter os maps imutáveis após criação', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ]); + + const pesosMetas = new Map([ + ['Meta A', 1], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + // Modificar os maps originais não deve afetar a instância + pesosConceitos.set('MA', 999); + pesosMetas.set('Meta A', 999); + + const json = especificacao.toJSON(); + expect(json.pesosDosConceitos.MA).toBe(10); // Deve manter o valor original + expect(json.pesosDasMetas['Meta A']).toBe(1); // Deve manter o valor original + }); + + // Teste 12: Cálculo com notas vazias + it('deve retornar 0 quando não há notas', () => { + const pesosConceitos = new Map([ + ['MA', 10], + ['MPA', 7], + ['MANA', 0], + ]); + + const pesosMetas = new Map([ + ['Meta A', 2], + ['Meta B', 3], + ]); + + const especificacao = new EspecificacaoDoCalculoDaMedia(pesosConceitos, pesosMetas); + + const notasVazias = new Map(); + const media = especificacao.calc(notasVazias); + + expect(media).toBe(0); + }); +}); \ No newline at end of file From 2fba249a3a12aabb1ef16fa0efc2c2ea15bfb742 Mon Sep 17 00:00:00 2001 From: jambis-prg Date: Wed, 10 Dec 2025 06:23:23 -0300 Subject: [PATCH 3/3] test(EspecificacaoDoCalculoDaMedia): integration with API and Class implemented and tested --- ...cificacao_calculo_media_integracao.test.ts | 326 ++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 server/src/__tests__/especificacao_calculo_media_integracao.test.ts diff --git a/server/src/__tests__/especificacao_calculo_media_integracao.test.ts b/server/src/__tests__/especificacao_calculo_media_integracao.test.ts new file mode 100644 index 00000000..5772f37e --- /dev/null +++ b/server/src/__tests__/especificacao_calculo_media_integracao.test.ts @@ -0,0 +1,326 @@ +// especificacao_calculo_media.integration.test.ts +import request from 'supertest'; +import { app, studentSet, classes } from '../server'; +import { Student } from '../models/Student'; +import { Class } from '../models/Class'; +import { EspecificacaoDoCalculoDaMedia, DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA, Grade } from '../models/EspecificacaoDoCalculoDaMedia'; + +// Mock para desabilitar persistência durante testes +jest.mock('fs', () => ({ + existsSync: jest.fn(), + readFileSync: jest.fn(), + writeFileSync: jest.fn(), + mkdirSync: jest.fn(), +})); + +describe('EspecificacaoDoCalculoDaMedia - Testes de Integração (Servidor)', () => { + let testStudent: Student; + let testClass: Class; + let customEspecificacao: EspecificacaoDoCalculoDaMedia; + + beforeEach(() => { + // Limpar dados antes de cada teste + studentSet.getAllStudents().forEach(student => { + studentSet.removeStudent(student.getCPF()); + }); + + classes.getAllClasses().forEach(classObj => { + classes.removeClass(classObj.getClassId()); + }); + + // Criar estudante de teste + testStudent = new Student('João Teste', '111.222.333-44', 'joao@teste.com'); + studentSet.addStudent(testStudent); + + // Criar especificação customizada + customEspecificacao = EspecificacaoDoCalculoDaMedia.fromJSON({ + pesosDosConceitos: { MA: 10, MPA: 8, MANA: 4 }, + pesosDasMetas: { + 'Meta 1': 2, + 'Meta 2': 3, + 'Meta 3': 5 + } + }); + + // Criar turma de teste com especificação padrão + testClass = new Class('Teste Integração', 1, 2024, DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA); + classes.addClass(testClass); + }); + + afterEach(() => { + // Limpar após cada teste + jest.clearAllMocks(); + }); + + // Teste 1: Criar turma com especificação padrão via API + it('deve criar uma turma com especificação padrão via API', async () => { + const response = await request(app) + .post('/api/classes') + .send({ + topic: 'Nova Turma', + semester: 1, + year: 2024 + }); + + expect(response.status).toBe(201); + expect(response.body.topic).toBe('Nova Turma'); + expect(response.body.especificacaoDoCalculoDaMedia).toBeDefined(); + + // Verificar que a especificação padrão foi usada + const especificacao = response.body.especificacaoDoCalculoDaMedia; + expect(especificacao.pesosDosConceitos.MA).toBe(10); + expect(especificacao.pesosDosConceitos.MPA).toBe(7); + expect(especificacao.pesosDosConceitos.MANA).toBe(0); + }); + + // Teste 2: Obter turma e verificar especificação + it('deve retornar a especificação ao obter uma turma', async () => { + // Primeiro criar uma turma + const createResponse = await request(app) + .post('/api/classes') + .send({ + topic: 'Turma Teste', + semester: 1, + year: 2024 + }); + + const classId = createResponse.body.id; + + // Obter todas as turmas + const response = await request(app) + .get('/api/classes'); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + + // Encontrar a turma criada + const turmaCriada = response.body.find((c: any) => c.id === classId); + expect(turmaCriada).toBeDefined(); + expect(turmaCriada.especificacaoDoCalculoDaMedia).toBeDefined(); + }); + + // Teste 3: Cálculo direto usando especificação (teste de integração sem API) + describe('Testes Diretos de Cálculo com Especificação', () => { + it('deve calcular média corretamente usando especificação da turma', () => { + // Criar especificação customizada + const especificacao = new EspecificacaoDoCalculoDaMedia( + new Map([['MA', 10], ['MPA', 7], ['MANA', 0]]), + new Map([ + ['Meta 1', 2], + ['Meta 2', 3], + ['Meta 3', 5] + ]) + ); + + // Criar turma com essa especificação + const turma = new Class('Turma Cálculo', 1, 2024, especificacao); + + // Matricular aluno + const enrollment = turma.addEnrollment(testStudent); + + // Atribuir notas + enrollment.addOrUpdateEvaluation('Meta 1', 'MA'); + enrollment.addOrUpdateEvaluation('Meta 2', 'MPA'); + enrollment.addOrUpdateEvaluation('Meta 3', 'MANA'); + + // Obter avaliações e calcular média + const evaluations = enrollment.getEvaluations(); + const notasDasMetas = new Map(); + evaluations.forEach(evaluation => { + notasDasMetas.set(evaluation.getGoal(), evaluation.getGrade()); + }); + + // Calcular média usando a especificação da turma + const media = turma.getEspecificacaoDoCalculoDaMedia().calc(notasDasMetas); + + // Verificar cálculo: (2*10 + 3*7 + 5*0) / (2+3+5) = (20 + 21 + 0) / 10 = 41/10 = 4.1 + expect(media).toBe(4.1); + }); + + // Teste 9: Testar com especificação diferente + it('deve calcular médias diferentes com especificações diferentes', () => { + // Especificação 1 (padrão) + const especificacao1 = DEFAULT_ESPECIFICACAO_DO_CALCULO_DA_MEDIA; + + // Especificação 2 (mais branda - MANA vale 4) + const especificacao2 = new EspecificacaoDoCalculoDaMedia( + new Map([['MA', 10], ['MPA', 8], ['MANA', 4]]), + new Map([ + ['Gerência de Configuração', 1], + ['Gerência de Projeto', 1], + ['Qualidade de Software', 1] + ]) + ); + + // Mesmas notas + const notas = new Map([ + ['Gerência de Configuração', 'MA'], + ['Gerência de Projeto', 'MPA'], + ['Qualidade de Software', 'MANA'] + ]); + + const media1 = especificacao1.calc(notas); + const media2 = especificacao2.calc(notas); + + // A especificação mais branda deve dar média maior + expect(media2).toBeGreaterThan(media1); + + // Cálculos específicos: + // especificacao1: (10*1 + 7*1 + 0*1) / 3 = 17/3 ≈ 5.6667 + // especificacao2: (10*1 + 8*1 + 4*1) / 3 = 22/3 ≈ 7.3333 + expect(media1).toBeCloseTo(5.6667, 4); + expect(media2).toBeCloseTo(7.3333, 4); + }); + + // Teste 4: Tentar usar especificação inválida + it('deve rejeitar especificação inválida', () => { + // Teste direto na classe (não via API pois a API não permite customização ainda) + expect(() => { + new EspecificacaoDoCalculoDaMedia( + new Map([['MA', 10]]), + new Map() // Metas vazias - soma zero + ); + }).toThrow('A soma dos pesos das metas não pode ser zero.'); + }); + + // Teste 5: Serialização/Deserialização via JSON da API + it('deve manter consistência na serialização JSON', async () => { + // Criar turma + const response = await request(app) + .post('/api/classes') + .send({ + topic: 'Turma Serialização', + semester: 2, + year: 2024 + }); + + const json = response.body; + + // Verificar estrutura da especificação + expect(json.especificacaoDoCalculoDaMedia).toHaveProperty('pesosDosConceitos'); + expect(json.especificacaoDoCalculoDaMedia).toHaveProperty('pesosDasMetas'); + + // Verificar tipos + expect(typeof json.especificacaoDoCalculoDaMedia.pesosDosConceitos).toBe('object'); + expect(typeof json.especificacaoDoCalculoDaMedia.pesosDasMetas).toBe('object'); + + // Poderíamos reconstruir a especificação a partir do JSON + const especificacaoReconstruida = EspecificacaoDoCalculoDaMedia.fromJSON( + json.especificacaoDoCalculoDaMedia + ); + + expect(especificacaoReconstruida).toBeInstanceOf(EspecificacaoDoCalculoDaMedia); + }); + // Teste 10: Validação de especificação inválida + it('deve rejeitar especificação inválida', () => { + expect(() => { + new EspecificacaoDoCalculoDaMedia( + new Map([['MA', 10]]), + new Map() // Metas vazias - soma zero + ); + }).toThrow('A soma dos pesos das metas não pode ser zero.'); + }); + + // Teste 11: Cálculo com notas incompletas + it('deve calcular média mesmo com notas incompletas', () => { + const especificacao = new EspecificacaoDoCalculoDaMedia( + new Map([['MA', 10], ['MPA', 7], ['MANA', 0]]), + new Map([ + ['Meta 1', 1], + ['Meta 2', 2], + ['Meta 3', 3] + ]) + ); + + // Apenas duas das três metas têm notas + const notas = new Map([ + ['Meta 1', 'MA'], // 1 * 10 = 10 + ['Meta 2', 'MPA'], // 2 * 7 = 14 + // Meta 3 não tem nota + ]); + + // O método calc usa apenas as metas que têm notas + const media = especificacao.calc(notas); + + // (10 + 14) / (1 + 2 + 3) = 24 / 6 = 4 + expect(media).toBe(4); + }); + }); + + /* + // Teste 6: Relatório da turma deve considerar a especificação + it('deve gerar relatório considerando a especificação da turma', async () => { + // Criar turma + const createResponse = await request(app) + .post('/api/classes') + .send({ + topic: 'Turma Relatório', + semester: 1, + year: 2024 + }); + + const classId = createResponse.body.id; + + // Criar e matricular mais alunos + const alunos = [ + new Student('Aluno A', '222.333.444-55', 'a@teste.com'), + new Student('Aluno B', '333.444.555-66', 'b@teste.com'), + new Student('Aluno C', '444.555.666-77', 'c@teste.com') + ]; + + alunos.forEach(aluno => { + studentSet.addStudent(aluno); + }); + + // Matricular alunos + for (const aluno of alunos) { + await request(app) + .post(`/api/classes/${classId}/enroll`) + .send({ + studentCPF: aluno.getCPF() + }); + } + + // Gerar relatório + const response = await request(app) + .get(`/api/classes/${classId}/report`); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('class'); + expect(response.body).toHaveProperty('statistics'); + + // O relatório deve incluir a especificação + expect(response.body.class.especificacaoDoCalculoDaMedia).toBeDefined(); + }); + */ + + // Teste 8: Atualização de turma mantém especificação + it('deve manter a especificação ao atualizar uma turma', async () => { + // Criar turma + const createResponse = await request(app) + .post('/api/classes') + .send({ + topic: 'Turma Original', + semester: 1, + year: 2024 + }); + + const classId = createResponse.body.id; + const especificacaoOriginal = createResponse.body.especificacaoDoCalculoDaMedia; + + // Atualizar turma + const updateResponse = await request(app) + .put(`/api/classes/${classId}`) + .send({ + topic: 'Turma Atualizada', + semester: 2, + year: 2025 + }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.topic).toBe('Turma Atualizada'); + + // A especificação deve permanecer a mesma + expect(updateResponse.body.especificacaoDoCalculoDaMedia).toEqual(especificacaoOriginal); + }); +}); \ No newline at end of file