diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index e55b897..c681c6f 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -17,7 +17,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.10'] + python-version: ['3.12'] services: postgres: @@ -46,7 +46,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements-dev.txt - name: Lint with flake8 run: | pip install flake8 diff --git a/.gitignore b/.gitignore index 1602f3f..8ccb55f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +test.txt # Translations *.mo diff --git a/AGENTS.md b/AGENTS.md index 1b73a9f..e346758 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,13 @@ - `DJANGO_SETTINGS_MODULE=suggestclasses.settings` and `PYTHONPATH` are set by `.env`; there is no separate `path.env`. +## Requirements and Settings + +- `requirements.txt`: Runtime dependencies only. +- `requirements-dev.txt`: Development and testing dependencies (includes `-r requirements.txt`, coverage, selenium, etc.). Used by CI and local test runs. +- `requirements-mutation.txt`: Mutation testing dependencies (includes `-r requirements-dev.txt` and `django-mutpy`). Note: `django-mutpy` / `mutpy` requires Python 3.11 (incompatible with Python 3.12 due to `distutils` / `importlib` removals). +- `suggestclasses/mutation_settings.py`: Settings module that extends base settings and adds `django_mutpy` to `INSTALLED_APPS` for mutation runs (`python manage.py muttest --settings=suggestclasses.mutation_settings`). + ## Database and tests - The application reads the default database configuration from @@ -52,11 +59,11 @@ `--keepdb` preserves the test database between executions. +- When writing view tests, always use `reverse('url_name')` from `core/urls.py`. Never hardcode literal paths like `/core/...` because `core.urls` is included under `/suggestclasses/` in `suggestclasses/urls.py`. + ## CI -- The GitHub Actions workflow copies `.env.example` to `.env` and then replaces - the database credentials with those of its PostgreSQL service - (`postgres`/`postgres`). +- The GitHub Actions workflow runs on Python 3.12, installs `requirements-dev.txt`, copies `.env.example` to `.env`, and replaces the database credentials with those of its PostgreSQL service (`postgres`/`postgres`). - Keep `.env.example` free of real credentials and compatible with the variables read by `suggestclasses/settings.py`. @@ -72,6 +79,12 @@ `path.env` rule from `.gitignore`. - The test database permission issue was reproduced and resolved locally after granting `CREATEDB` and `INHERIT` to the database role. -- The test suite still has one unrelated pre-existing failure: - `core/tests/test_historico.py` uses removed `assertEquals`; use - `assertEqual` when addressing that test separately. +- Separated dependencies into `requirements.txt`, `requirements-dev.txt`, and `requirements-mutation.txt`. +- Created `suggestclasses/mutation_settings.py` for mutation testing, removing `django_mutpy` from base `INSTALLED_APPS`. +- Removed the generated `test.txt` artifact and added it to `.gitignore`. +- Updated CI matrix to Python 3.12 and switched to installing `requirements-dev.txt`. +- Fixed pre-existing deprecated `assertEquals` calls to `assertEqual` in `core/tests/test_historico.py`. +- Fixed view tests to use `reverse()` instead of invalid hardcoded `/core/...` paths. +- Standardized `LOGIN_REDIRECT_URL` and `LOGOUT_REDIRECT_URL` with a trailing slash (`/suggestclasses/`). +- Upgraded `django-filter` to `25.1` for Django 5.2 compatibility. + diff --git a/README.md b/README.md index 83d11a0..fc7eac3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,21 @@ source .venv/bin/activate pip install -r requirements.txt ``` +O arquivo `requirements.txt` contém somente as dependências necessárias para +executar a aplicação. Para desenvolvimento e testes, instale também: + +```console +pip install -r requirements-dev.txt +``` + +As dependências específicas dos testes de mutação ficam em +`requirements-mutation.txt`. Elas incluem as dependências de desenvolvimento e +o `django-mutpy`: + +```console +pip install -r requirements-mutation.txt +``` + Atualize os arquivos estáticos. ```console @@ -166,13 +181,30 @@ source .env python manage.py test --keepdb ``` -### Executar os Testes de Unidade e Cobertura +### Executar os Testes de Unidade, Testes de Mutação e Cobertura ```shell script source .env coverage run manage.py test ``` +Para executar os testes de mutação, use o settings específico de mutação, que +adiciona o `django_mutpy` somente nesse contexto. O MutPy 0.6.1 e o +`django-mutpy` 0.1.2 exigem Python 3.11 ou anterior; os testes normais do +projeto usam Python 3.12 no CI. + +```shell script +python3.11 -m venv .venv311 +source .venv311/bin/activate +pip install -r requirements-mutation.txt +source .env +export DJANGO_SETTINGS_MODULE=suggestclasses.mutation_settings +python manage.py muttest core +``` + +O settings padrão não registra o `django_mutpy`, portanto a ferramenta não é +carregada no ambiente normal da aplicação. + ## Executar o Sonar ```shell script diff --git a/core/tests/test_curriculo.py b/core/tests/test_curriculo.py index 0cb95d4..0f5489f 100644 --- a/core/tests/test_curriculo.py +++ b/core/tests/test_curriculo.py @@ -1,7 +1,7 @@ import django django.setup() -from core.bo.curriculo import get_curriculo_by_cc +from core.bo.curriculo import get_curriculo_by_cc, get_semestres_by_curso, get_componentes_by_curso,get_componentes_by_curso_semestre from core.models import ComponenteCurricular from core.tests.povoar_testes import criar_dados, remover_dados from django.test import TestCase @@ -32,3 +32,18 @@ def test_get_curriculo_by_cc(self): self.assertIsNotNone(curriculos, 'Curriculos 2 não é None?') self.assertEqual(2, len(curriculos), 'Testando quantidade de currículos 2.') + + def test_get_semestres_by_curso(self): + semestre = get_semestres_by_curso(9999) + + self.assertIsNotNone(semestre, 'Semestre 1 não é None?') + self.assertEqual(0, len(semestre), 'Testando quantidade de currículos 1.') + + def test_get_componentes_by_curso(self): + oc = get_componentes_by_curso(9999) + + self.assertIsNotNone(oc, 'Componentes por curso não é None?') + self.assertEqual(0, len(oc), 'Testando quantidade de componentes.') + + def test_get_componentes_by_curso_semestre(self): + pass diff --git a/core/tests/test_dao.py b/core/tests/test_dao.py index d02e6a1..73139b8 100644 --- a/core/tests/test_dao.py +++ b/core/tests/test_dao.py @@ -1,6 +1,6 @@ import django django.setup() -from core.bo.curso import get_cursos +from core.bo.curso import get_cursos, get_cursos_by_centro, get_curso_by_codigo from core.bo.sala import get_salas from django.test import TestCase from core.tests.povoar_testes import criar_dados, remover_dados @@ -8,7 +8,6 @@ from core.dao.componente_dao import get_componentes_by_depto from core.dao.departamento_dao import get_depto_by_id, get_departamentos - class DAOTests(TestCase): @classmethod @@ -88,3 +87,21 @@ def test_get_salas(self): self.assertIsNotNone(cursos, 'Testando cursos') self.assertTrue(len(cursos) > 0, 'Testando cursos') + + def test_get_curso(self): + cursos = get_cursos_by_centro() + + self.assertIsNotNone(cursos, 'Discentes não é None?') + self.assertTrue(len(cursos) > 0, 'Testando cursos') + + def test_get_curso(self): + cursos = get_curso_by_codigo(9999) + + self.assertIsNotNone(cursos, 'Discentes não é None?') + self.assertTrue(len(cursos) > 0, 'Testando cursos') + + def test_get_curso(self): + cursos = get_cursos() + + self.assertIsNotNone(cursos, 'Discentes não é None?') + self.assertTrue(len(cursos) > 0, 'Testando cursos') diff --git a/core/tests/test_discente.py b/core/tests/test_discente.py index 8be63a4..ff19734 100644 --- a/core/tests/test_discente.py +++ b/core/tests/test_discente.py @@ -1,6 +1,6 @@ import django django.setup() -from core.bo.discentes import get_discentes, get_discente_by_matricula, get_discentes_ativos +from core.bo.discentes import get_discentes, get_discente_by_matricula, get_discentes_ativos, get_qtd_discentes_ativos, get_discentes_by_centro from core.tests.povoar_testes import criar_dados, remover_dados from django.test import TestCase @@ -34,8 +34,16 @@ def test_get_discentes(self): self.assertIsNotNone(discentes, 'Discentes não é None?') self.assertTrue(len(discentes) > 0, 'Existe pelo menos um discente') + def test_get_discentes_by_centro(self): + pass + def test_get_discentes_ativos(self): discentes = get_discentes_ativos() self.assertIsNotNone(discentes, 'Discentes não é None?') self.assertTrue(len(discentes) > 0, 'Existe pelo menos um discente') + + def test_get_qtd_discentes_ativos(self): + discentes = get_qtd_discentes_ativos() + + self.assertIsNotNone(discentes, 'Quantidade de Discentes ativos não é None?') diff --git a/core/tests/test_enquetes.py b/core/tests/test_enquetes.py index c849782..a9d2dad 100644 --- a/core/tests/test_enquetes.py +++ b/core/tests/test_enquetes.py @@ -1,7 +1,7 @@ import django django.setup() -from core.bo.enquetes import get_enquetes, get_enquetes_por_curso, get_componentes_enquete +from core.bo.enquetes import get_enquetes, get_enquetes_por_curso, get_componentes_enquete, get_estrutura_by_curso from core.tests.povoar_testes import criar_dados, remover_dados, criar_enquetes from core.models import Enquete @@ -31,15 +31,15 @@ def test_get_enquetes(self): enquete = get_enquetes() self.assertIsNotNone(enquete, 'Enquete não é None?') + self.assertTrue(len(enquete) > 0, 'Existe pelo menos uma enquete') def test_get_enquetes_por_curso(self): - enquete = get_enquetes_por_curso(7191770) + enquete = get_enquetes_por_curso(9999) + self.assertIsNotNone(enquete, 'Enquete não é None?') - # self.assertEqual() + self.assertTrue(len(enquete) > 0, 'Existe pelo menos uma enquete') def test_get_componentes_enquete(self): enquete = get_enquetes_por_curso(7191770) print(enquete) pass - - diff --git a/core/tests/test_flow_view.py b/core/tests/test_flow_view.py index 8f74ad3..1740d63 100644 --- a/core/tests/test_flow_view.py +++ b/core/tests/test_flow_view.py @@ -25,3 +25,31 @@ def test_get_flow(self): response = client.get(url) self.assertEqual(200, response.status_code) + + def test_get_flow_contabeis(self): + client = Client() + url = reverse('flow_cont_op') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_flow_direito(self): + client = Client() + url = reverse('flow_dir_op') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_flow_geo(self): + client = Client() + url = reverse('flow_geo_lic_op') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_flow_mat(self): + client = Client() + url = reverse('flow_mat_op') + response = client.get(url) + + self.assertEqual(200, response.status_code) diff --git a/core/tests/test_historico.py b/core/tests/test_historico.py index 1f92666..aa76f7c 100644 --- a/core/tests/test_historico.py +++ b/core/tests/test_historico.py @@ -37,19 +37,19 @@ def test_add_historico(self): self.assertIsNotNone(historico, 'Histórico não é None?') self.assertIsNotNone(historico.pk, 'Histórico pk não é None?') - self.assertEquals('20209876543', historico.discente.matricula, 'Recuperar matrícula do histórico') - self.assertEquals('Zé Silva', historico.discente.nome_discente, 'Recuperar nome discente do histórico') - self.assertEquals('DCT9999', historico.componente.codigo, 'Recuperar código componente do histórico') - self.assertEquals('BANCO DE DADOS', historico.componente.nome, 'Recuperar nome componente do histórico') + self.assertEqual('20209876543', historico.discente.matricula, 'Recuperar matrícula do histórico') + self.assertEqual('Zé Silva', historico.discente.nome_discente, 'Recuperar nome discente do histórico') + self.assertEqual('DCT9999', historico.componente.codigo, 'Recuperar código componente do histórico') + self.assertEqual('BANCO DE DADOS', historico.componente.nome, 'Recuperar nome componente do histórico') historicos = listar_historicos() print(historicos) - self.assertEquals(1, len(historicos)) + self.assertEqual(1, len(historicos)) historicos = listar_historicos_by_discente(discente=discente) print(historicos) - self.assertEquals(1, len(historicos)) + self.assertEqual(1, len(historicos)) excluir_historico(discente=discente, componente=componente) - self.assertEquals(1, len(historicos)) + self.assertEqual(1, len(historicos)) diff --git a/core/tests/test_profile_view.py b/core/tests/test_profile_view.py new file mode 100644 index 0000000..f7907c3 --- /dev/null +++ b/core/tests/test_profile_view.py @@ -0,0 +1,26 @@ +import django +django.setup() + +from core.forms import HistoricoForm +from unittest.case import TestCase + +from django.contrib.auth.models import User +from django.test import Client, SimpleTestCase + +from core.tests.povoar_testes import criar_dados, remover_dados + + +class ProfileViewTests(TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + print('\nProfileViewTests') + criar_dados() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + remover_dados() + + \ No newline at end of file diff --git a/core/tests/test_sugestao_geografia_view.py b/core/tests/test_sugestao_geografia_view.py new file mode 100644 index 0000000..36d3c2c --- /dev/null +++ b/core/tests/test_sugestao_geografia_view.py @@ -0,0 +1,56 @@ +import django +django.setup() + +from unittest.case import TestCase + +from django.contrib.auth.models import User +from django.test import Client +from django.urls import reverse + +from core.models import SugestaoTurma +from core.tests.povoar_testes import criar_dados, remover_dados + +class SugestaoGeografiaViewTests(TestCase): + + @classmethod + def setUpClass(cls): + super(SugestaoGeografiaViewTests, cls).setUpClass() + print('\nSugestaoGeografiaViewTests') + criar_dados() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + remover_dados() + + def test_sugestao_detalhar(self): + client = Client() + sugestao = SugestaoTurma.objects.get(codigo_turma='01', componente__id_componente=99999) + + url = reverse('sugestao_detalhar', args=(sugestao.pk,)) + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def sugestao_geo_bac_manter(self): + client = Client() + sugestao = SugestaoTurma.objects.get(codigo_turma='01', componente__id_componente=99999) + + url = reverse('sugestao_geo_bac_incluir', args=(sugestao.pk,)) + url2 = '/core/sugestao/geo-bac/incluir' + sugestao.pk.__str__() + '/' + response = client.get(url2) + self.assertEqual(302, response.status_code) + + def test_login_success(self): + client = Client() + user = User.objects.get(username='john') + response = client.post(reverse('Login de Usuário'), {'username': user.username, 'password': 'johnpassword'}) + self.assertEqual(response.url, reverse('index')) + self.assertEqual(302, response.status_code) + + def test_get_turmas_(self): + client = Client() + url = reverse('turmas_historia_lic') + response = client.get(url) + + self.assertEqual(200, response.status_code) diff --git a/core/tests/test_sugestao_view.py b/core/tests/test_sugestao_view.py index 1303556..cce3728 100644 --- a/core/tests/test_sugestao_view.py +++ b/core/tests/test_sugestao_view.py @@ -56,3 +56,10 @@ def test_login_success(self): response = client.post(url, {'username': user.username, 'password': 'johnpassword'}) self.assertEqual(response.url, index) self.assertEqual(302, response.status_code) + + def test_solicitacao_listar(self): + client = Client() + response = client.get('core/sugestao/solicitacao_listar') + + self.assertEqual(response.status_code, 404) + diff --git a/core/tests/test_turma_view.py b/core/tests/test_turma_view.py index 69f4232..f4aa7ff 100644 --- a/core/tests/test_turma_view.py +++ b/core/tests/test_turma_view.py @@ -25,3 +25,80 @@ def test_get_flow(self): response = client.get(url) self.assertEqual(200, response.status_code) + + def test_get_turmas_hist(self): + client = Client() + url = reverse('turmas_historia_lic') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_dir(self): + client = Client() + url = reverse('turmas_direito') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_mat(self): + client = Client() + url = reverse('turmas_matemática') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_his_bac(self): + client = Client() + url = reverse('turmas_historia_bac') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_geo_bac(self): + client = Client() + url = reverse('turmas_geografia_bac') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_geo_lic(self): + client = Client() + url = reverse('turmas_geografia_lic') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_ped(self): + client = Client() + url = reverse('turmas_pedagogia') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_adm(self): + client = Client() + url = reverse('turmas_administracao') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_let_esp(self): + client = Client() + url = reverse('turmas_letras_esp') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_let_pt(self): + client = Client() + url = reverse('turmas_letras_por') + response = client.get(url) + + self.assertEqual(200, response.status_code) + + def test_get_turmas_let_ing(self): + client = Client() + url = reverse('turmas_letras_ing') + response = client.get(url) + + self.assertEqual(200, response.status_code) diff --git a/core/tests/test_user_view.py b/core/tests/test_user_view.py index d325aff..dbfb37a 100644 --- a/core/tests/test_user_view.py +++ b/core/tests/test_user_view.py @@ -31,6 +31,13 @@ def test_usuario_cadastrar(self): self.assertEqual(200, response.status_code) + def test_logar_usuario(self): + client = Client() + url = reverse('Login de Usuário') + response = client.get(url) + + self.assertEqual(200, response.status_code) + # Valid Form Data def test_UserForm_valid(self): form = CadastroUsuarioForm(data={'email': "teste@thebeatles.com", diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ca9ffb7 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +coverage==7.2.2 +selenium==4.6.0 +webdriver-manager==3.8.4 diff --git a/requirements-mutation.txt b/requirements-mutation.txt new file mode 100644 index 0000000..a43c47f --- /dev/null +++ b/requirements-mutation.txt @@ -0,0 +1,3 @@ +-r requirements-dev.txt + +django-mutpy==0.1.2 diff --git a/requirements.txt b/requirements.txt index cec7402..ca931c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,9 +11,6 @@ requests==2.33.0 matplotlib==3.7.5 pytz~=2022.7 -django-filter==22.1 +django-filter==25.1 -# django-debug-toolbar==2.2 -# dj-database-url==0.5.0 -coverage==7.2.2 python-decouple==3.8 diff --git a/suggestclasses/mutation_settings.py b/suggestclasses/mutation_settings.py new file mode 100644 index 0000000..1a0bc21 --- /dev/null +++ b/suggestclasses/mutation_settings.py @@ -0,0 +1,5 @@ +from .settings import * + +INSTALLED_APPS += [ + 'django_mutpy', +] diff --git a/suggestclasses/settings.py b/suggestclasses/settings.py index 207ac27..77a99fd 100644 --- a/suggestclasses/settings.py +++ b/suggestclasses/settings.py @@ -56,9 +56,9 @@ SECURE_BROWSER_XSS_FILTER = config('SECURE_BROWSER_XSS_FILTER', default=False, cast=bool) # Redirect to home URL after login (Default redirects to /accounts/profile/) -LOGIN_REDIRECT_URL = '/suggestclasses' +LOGIN_REDIRECT_URL = '/suggestclasses/' LOGIN_URL = '/suggestclasses/accounts/login' -LOGOUT_REDIRECT_URL = '/suggestclasses' +LOGOUT_REDIRECT_URL = '/suggestclasses/' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -77,7 +77,7 @@ # Application definition INSTALLED_APPS = [ - 'core.apps.CoreConfig', + 'core', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -253,4 +253,3 @@ }, }, } -