diff --git a/.github/workflows/run-apps-tests.yml b/.github/workflows/run-apps-tests.yml new file mode 100644 index 00000000..fac18a39 --- /dev/null +++ b/.github/workflows/run-apps-tests.yml @@ -0,0 +1,40 @@ +name: Run Apps Tests + +on: + push: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + + services: + sqlite: + image: python:3 + env: + SQLITE_DB: test_db.sqlite3 + ports: + - 5432:5432 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Prepare SQLite database + run: | + python manage.py migrate + + - name: Run tests + run: | + python manage.py test + diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/fsm/tests/__init__.py b/apps/fsm/tests/__init__.py index f984bf6b..e69de29b 100644 --- a/apps/fsm/tests/__init__.py +++ b/apps/fsm/tests/__init__.py @@ -1,2 +0,0 @@ -from fsm.tests.invitation_tests import InvitationTest -from fsm.tests import registration_tests \ No newline at end of file diff --git a/apps/fsm/tests/invitation_tests.py b/apps/fsm/tests/test_invitation.py similarity index 100% rename from apps/fsm/tests/invitation_tests.py rename to apps/fsm/tests/test_invitation.py diff --git a/apps/fsm/tests/registration_tests.py b/apps/fsm/tests/test_registration.py similarity index 100% rename from apps/fsm/tests/registration_tests.py rename to apps/fsm/tests/test_registration.py diff --git a/apps/report/tests.py b/apps/report/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/report/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/report/tests/__init__.py b/apps/report/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/report/tests/test_health_check.py b/apps/report/tests/test_health_check.py new file mode 100644 index 00000000..5f6fd03a --- /dev/null +++ b/apps/report/tests/test_health_check.py @@ -0,0 +1,65 @@ +from django.test import TestCase, Client, override_settings +from django.urls import reverse +from django.apps import apps +import csv + + +class ViewTestCase(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.client = Client() + + def setUp(self): + pass + + def tearDown(self): + pass + + +class ExportViewTest(ViewTestCase): + def test_export_view(self): + with override_settings(ROOT_URLCONF='report.urls'): + url = reverse('export_json') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json') + + self.assertTrue(response.content) + + self.assertTrue('Content-Disposition' in response) + + self.assertTrue('attachment' in response['Content-Disposition']) + self.assertTrue( + 'exported_data.json' in response['Content-Disposition']) + + +class ExportCSVViewTest(ViewTestCase): + def test_export_csv_view(self): + with override_settings(ROOT_URLCONF='report.urls'): + + url = reverse('export_csv') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response['Content-Type'], 'text/csv') + + self.assertTrue(response.content) + + self.assertTrue('Content-Disposition' in response) + + self.assertTrue('attachment' in response['Content-Disposition']) + + self.assertTrue( + 'exported_data.csv' in response['Content-Disposition']) + csv_data = response.content.decode('utf-8').splitlines() + csv_reader = csv.reader(csv_data) + header_row = next(csv_reader) + expected_header = [f"{model.__name__}_{field.name}" for model in apps.get_models( + ) for field in model._meta.fields] + self.assertEqual(header_row, expected_header) + num_rows = len(list(csv_reader)) + expected_num_rows = sum(model.objects.count() + for model in apps.get_models()) + self.assertEqual(num_rows, expected_num_rows) diff --git a/apps/roadmap/tests.py b/apps/roadmap/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/roadmap/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/roadmap/tests/__init__.py b/apps/roadmap/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/roadmap/tests/test_health_check.py b/apps/roadmap/tests/test_health_check.py new file mode 100644 index 00000000..6e596236 --- /dev/null +++ b/apps/roadmap/tests/test_health_check.py @@ -0,0 +1,39 @@ +from django.test import TestCase +from rest_framework.test import APIRequestFactory +from rest_framework import status +from unittest.mock import patch, MagicMock +from apps.roadmap.views import get_player_taken_path, get_fsm_roadmap + + +class TestRoadmapViewsHealthCheck(TestCase): + def setUp(self): + pass + + def test_get_player_taken_path(self): + with patch('apps.roadmap.views.Player.get_player') as mocked_get_player: + player_instance = MagicMock() + player_instance.current_state.fsm = MagicMock() + mocked_get_player.return_value = player_instance + + # Mocking _get_previous_taken_state + with patch('apps.roadmap.views._get_previous_taken_state') as mocked_prev_taken_state: + mocked_prev_taken_state.return_value = None + + data = {'player_id': 1} + request = APIRequestFactory().post('get_player_taken_path/', data) + response = get_player_taken_path(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_get_fsm_roadmap(self): + with patch('apps.roadmap.views.FSM.get_fsm') as mocked_get_fsm, \ + patch('apps.roadmap.views._get_fsm_edges') as mocked_get_fsm_edges: + fsm_instance = MagicMock() + fsm_instance.first_state.name = "First State" + mocked_get_fsm.return_value = fsm_instance + mocked_get_fsm_edges.return_value = [ + MagicMock(tail=MagicMock(), head=MagicMock())] + + data = {'fsm_id': 1} + request = APIRequestFactory().post('get_fsm_roadmap/', data) + response = get_fsm_roadmap(request) + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/roadmap/utils.py b/apps/roadmap/utils.py new file mode 100644 index 00000000..485632e6 --- /dev/null +++ b/apps/roadmap/utils.py @@ -0,0 +1,47 @@ + +from apps.fsm.views.fsm_view import _get_fsm_edges + +from apps.roadmap.models import Link +from apps.fsm.models import FSM, Player, PlayerHistory, State + + +def _get_fsm_links(fsm_id: int): + fsm = FSM.get_fsm(fsm_id) + edges = _get_fsm_edges(fsm) + links = [Link.get_link_from_states( + edge.tail, edge.head) for edge in edges] + return links + + +def _get_player_taken_path(player_id: int): + player = Player.get_player(player_id) + player_current_state: State = player.current_state + fsm = player_current_state.fsm + histories: list[PlayerHistory] = player.histories.all() + taken_path: list[Link] = [] + + # 100 is consumed as maximum length in a fsm graph + for i in range(100): + previous_state = _get_previous_taken_state( + player_current_state, histories) + # if the entered_by_edge is deleted, it isn't possible to reach to previous state + if not previous_state: + break + taken_path.append(Link.get_link_from_states( + previous_state, player_current_state)) + player_current_state = previous_state + + taken_path.reverse() + return taken_path + + +def _get_previous_taken_state(player_current_state: State, histories: list[PlayerHistory]): + for history in histories: + if history.reverse_enter: + continue + # if the entered_by_edge is deleted: + if not history.entered_by_edge: + continue + if history.entered_by_edge.head == player_current_state: + return history.entered_by_edge.tail + return None diff --git a/apps/roadmap/views.py b/apps/roadmap/views.py index 635a1ec1..cc0227fa 100644 --- a/apps/roadmap/views.py +++ b/apps/roadmap/views.py @@ -1,14 +1,13 @@ -import json from rest_framework import status from rest_framework.response import Response from rest_framework.response import Response from rest_framework.decorators import api_view -from apps.fsm.views.fsm_view import _get_fsm_edges from apps.roadmap.models import Link -from apps.fsm.models import FSM, Player, PlayerHistory, State +from apps.fsm.models import FSM from apps.roadmap.serializers import LinkSerializer +from apps.roadmap.utils import _get_fsm_links, _get_player_taken_path @api_view(["POST"]) @@ -24,45 +23,3 @@ def get_fsm_roadmap(request): fsm = FSM.get_fsm(fsm_id) fsm_links = _get_fsm_links(fsm_id) return Response(data={'first_state_name': fsm.first_state.name, 'links': LinkSerializer(fsm_links, many=True).data}, status=status.HTTP_200_OK) - - -def _get_fsm_links(fsm_id: int): - fsm = FSM.get_fsm(fsm_id) - edges = _get_fsm_edges(fsm) - links = [Link.get_link_from_states( - edge.tail, edge.head) for edge in edges] - return links - - -def _get_player_taken_path(player_id: int): - player = Player.get_player(player_id) - player_current_state: State = player.current_state - fsm = player_current_state.fsm - histories: list[PlayerHistory] = player.histories.all() - taken_path: list[Link] = [] - - # 100 is consumed as maximum length in a fsm graph - for i in range(100): - previous_state = _get_previous_taken_state( - player_current_state, histories) - # if the entered_by_edge is deleted, it isn't possible to reach to previous state - if not previous_state: - break - taken_path.append(Link.get_link_from_states( - previous_state, player_current_state)) - player_current_state = previous_state - - taken_path.reverse() - return taken_path - - -def _get_previous_taken_state(player_current_state: State, histories: list[PlayerHistory]): - for history in histories: - if history.reverse_enter: - continue - # if the entered_by_edge is deleted: - if not history.entered_by_edge: - continue - if history.entered_by_edge.head == player_current_state: - return history.entered_by_edge.tail - return None