From d9114c900db5e2ab6fd305145f364baacc774829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20=C5=A0im=C3=A1nek?= Date: Sun, 2 Nov 2025 13:51:29 +0100 Subject: [PATCH 1/6] API v1 initial tests. --- .github/workflows/ci.yaml | 50 ++++++ pgcommitfest/commitfest/tests/__init__.py | 1 + pgcommitfest/commitfest/tests/test_apiv1.py | 159 ++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 pgcommitfest/commitfest/tests/__init__.py create mode 100644 pgcommitfest/commitfest/tests/test_apiv1.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2beb0666..2fb90774 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,3 +33,53 @@ jobs: - name: Run djhtml run: djhtml pgcommitfest/*/templates/*.html pgcommitfest/*/templates/*.inc --tabwidth=1 --check + + test: + runs-on: ubuntu-24.04 + name: "Django Tests" + + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python with uv + uses: astral-sh/setup-uv@v4 + + - name: Create CI settings + run: | + cat > pgcommitfest/local_settings.py << 'EOF' + # CI test settings + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": "pgcommitfest", + "USER": "postgres", + "PASSWORD": "postgres", + "HOST": "localhost", + "PORT": "5432", + } + } + + # Enable debug for better error messages in CI + DEBUG = True + EOF + + - name: Install dependencies + run: uv sync + + - name: Run tests + run: uv run manage.py test --verbosity=2 diff --git a/pgcommitfest/commitfest/tests/__init__.py b/pgcommitfest/commitfest/tests/__init__.py new file mode 100644 index 00000000..7d5a9d90 --- /dev/null +++ b/pgcommitfest/commitfest/tests/__init__.py @@ -0,0 +1 @@ +# Tests for the commitfest application diff --git a/pgcommitfest/commitfest/tests/test_apiv1.py b/pgcommitfest/commitfest/tests/test_apiv1.py new file mode 100644 index 00000000..19633386 --- /dev/null +++ b/pgcommitfest/commitfest/tests/test_apiv1.py @@ -0,0 +1,159 @@ +from django.test import Client, TestCase, override_settings + +import json +from datetime import date, timedelta + +from pgcommitfest.commitfest.models import CommitFest + + +@override_settings(AUTO_CREATE_COMMITFESTS=False) +class NeedsCIEndpointTestCase(TestCase): + """Test the /api/v1/commitfests/needs_ci endpoint.""" + + @classmethod + def setUpTestData(cls): + today = date.today() + + # Create test commitfests with various statuses + cls.open_cf = CommitFest.objects.create( + name="2025-01", + status=CommitFest.STATUS_OPEN, + startdate=today - timedelta(days=30), + enddate=today + timedelta(days=30), + draft=False, + ) + + cls.in_progress_cf = CommitFest.objects.create( + name="2024-11", + status=CommitFest.STATUS_INPROGRESS, + startdate=today - timedelta(days=60), + enddate=today + timedelta(days=0), + draft=False, + ) + + # Previous CF that ended 3 days ago (should be included - within 7 day window) + cls.recent_previous_cf = CommitFest.objects.create( + name="2024-09", + status=CommitFest.STATUS_CLOSED, + startdate=today - timedelta(days=90), + enddate=today - timedelta(days=3), + draft=False, + ) + + # Old previous CF that ended 10 days ago (should be excluded - outside 7 day window) + cls.old_previous_cf = CommitFest.objects.create( + name="2024-07", + status=CommitFest.STATUS_CLOSED, + startdate=today - timedelta(days=120), + enddate=today - timedelta(days=10), + draft=False, + ) + + # Draft commitfest + cls.draft_cf = CommitFest.objects.create( + name="2025-03-draft", + status=CommitFest.STATUS_OPEN, + startdate=today + timedelta(days=60), + enddate=today + timedelta(days=120), + draft=True, + ) + + def setUp(self): + self.client = Client() + + def test_endpoint_returns_200(self): + """Test that the endpoint returns HTTP 200 OK.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + self.assertEqual(response.status_code, 200) + + def test_response_is_valid_json(self): + """Test that the response is valid JSON.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + try: + data = json.loads(response.content) + except json.JSONDecodeError: + self.fail("Response is not valid JSON") + + self.assertIn("commitfests", data) + self.assertIsInstance(data["commitfests"], dict) + + def test_response_content_type(self): + """Test that the response has correct Content-Type header.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + self.assertEqual(response["Content-Type"], "application/json") + + def test_cors_header_present(self): + """Test that CORS header is present for API access.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + self.assertEqual(response["Access-Control-Allow-Origin"], "*") + + def test_includes_open_commitfest(self): + """Test that open commitfests are included in response.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + commitfests = data["commitfests"] + + # Should include the open commitfest + self.assertIn("open", commitfests) + self.assertEqual(commitfests["open"]["name"], self.open_cf.name) + + def test_includes_in_progress_commitfest(self): + """Test that in-progress commitfests are included in response.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + commitfests = data["commitfests"] + + # Should include the in-progress commitfest + self.assertEqual(commitfests["in_progress"]["name"], self.in_progress_cf.name) + + def test_includes_recent_previous_commitfest(self): + """Test that recently ended commitfests are included (within 7 days).""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + commitfests = data["commitfests"] + + # Should include recent previous commitfest (ended 3 days ago) + self.assertIsNotNone(commitfests["previous"]) + + def test_excludes_old_previous_commitfest(self): + """Test that old commitfests are excluded (older than 7 days).""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + commitfests = data["commitfests"] + + # Should not include old previous commitfest (ended 10 days ago) + self.assertNotEqual( + commitfests["previous"]["name"], + self.old_previous_cf.name, + "Old previous commitfest should be excluded", + ) + + def test_excludes_next_open_and_final(self): + """Test that next_open and final are excluded from response.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + commitfests = data["commitfests"] + + # These keys should not be present in the response + self.assertNotIn("next_open", commitfests) + self.assertNotIn("final", commitfests) + + def test_response_structure(self): + """Test that response has expected structure.""" + response = self.client.get("/api/v1/commitfests/needs_ci") + data = json.loads(response.content) + + # Top-level structure + self.assertIn("commitfests", data) + self.assertIsInstance(data["commitfests"], dict) + + # Check that commitfest objects have expected fields + commitfests = data["commitfests"] + for key, cf_data in commitfests.items(): + self.assertIsInstance(cf_data, dict) + # Basic fields that should be present + self.assertIn("id", cf_data) + self.assertIn("name", cf_data) + self.assertIn("status", cf_data) + self.assertIn("startdate", cf_data) + self.assertIn("enddate", cf_data) From 44548017293df9388812a0291ae9791f9f703ee4 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 10 Nov 2025 22:43:14 +0100 Subject: [PATCH 2/6] Use pytest for testing --- .github/workflows/ci.yaml | 5 +- pgcommitfest/commitfest/tests/test_apiv1.py | 209 +++++++------------- pyproject.toml | 12 ++ 3 files changed, 88 insertions(+), 138 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2fb90774..4537f617 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -81,5 +81,8 @@ jobs: - name: Install dependencies run: uv sync + - name: Run migrations + run: uv run manage.py migrate --noinput + - name: Run tests - run: uv run manage.py test --verbosity=2 + run: uv run pytest diff --git a/pgcommitfest/commitfest/tests/test_apiv1.py b/pgcommitfest/commitfest/tests/test_apiv1.py index 19633386..196d03c0 100644 --- a/pgcommitfest/commitfest/tests/test_apiv1.py +++ b/pgcommitfest/commitfest/tests/test_apiv1.py @@ -1,159 +1,94 @@ -from django.test import Client, TestCase, override_settings +from django.test import override_settings import json -from datetime import date, timedelta +from datetime import date -from pgcommitfest.commitfest.models import CommitFest +import pytest +from pgcommitfest.commitfest.models import CommitFest -@override_settings(AUTO_CREATE_COMMITFESTS=False) -class NeedsCIEndpointTestCase(TestCase): - """Test the /api/v1/commitfests/needs_ci endpoint.""" +pytestmark = pytest.mark.django_db - @classmethod - def setUpTestData(cls): - today = date.today() - # Create test commitfests with various statuses - cls.open_cf = CommitFest.objects.create( +@pytest.fixture +def commitfests(): + """Create test commitfests with various statuses.""" + return { + "open": CommitFest.objects.create( name="2025-01", status=CommitFest.STATUS_OPEN, - startdate=today - timedelta(days=30), - enddate=today + timedelta(days=30), + startdate=date(2025, 1, 1), + enddate=date(2025, 1, 31), draft=False, - ) - - cls.in_progress_cf = CommitFest.objects.create( + ), + "in_progress": CommitFest.objects.create( name="2024-11", status=CommitFest.STATUS_INPROGRESS, - startdate=today - timedelta(days=60), - enddate=today + timedelta(days=0), + startdate=date(2024, 11, 1), + enddate=date(2024, 11, 30), draft=False, - ) - - # Previous CF that ended 3 days ago (should be included - within 7 day window) - cls.recent_previous_cf = CommitFest.objects.create( + ), + "recent_previous": CommitFest.objects.create( name="2024-09", status=CommitFest.STATUS_CLOSED, - startdate=today - timedelta(days=90), - enddate=today - timedelta(days=3), + startdate=date(2024, 9, 1), + enddate=date(2024, 9, 30), draft=False, - ) - - # Old previous CF that ended 10 days ago (should be excluded - outside 7 day window) - cls.old_previous_cf = CommitFest.objects.create( + ), + "old_previous": CommitFest.objects.create( name="2024-07", status=CommitFest.STATUS_CLOSED, - startdate=today - timedelta(days=120), - enddate=today - timedelta(days=10), + startdate=date(2024, 7, 1), + enddate=date(2024, 7, 31), draft=False, - ) - - # Draft commitfest - cls.draft_cf = CommitFest.objects.create( + ), + "draft": CommitFest.objects.create( name="2025-03-draft", status=CommitFest.STATUS_OPEN, - startdate=today + timedelta(days=60), - enddate=today + timedelta(days=120), + startdate=date(2025, 3, 1), + enddate=date(2025, 3, 31), draft=True, - ) - - def setUp(self): - self.client = Client() - - def test_endpoint_returns_200(self): - """Test that the endpoint returns HTTP 200 OK.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - self.assertEqual(response.status_code, 200) - - def test_response_is_valid_json(self): - """Test that the response is valid JSON.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - try: - data = json.loads(response.content) - except json.JSONDecodeError: - self.fail("Response is not valid JSON") - - self.assertIn("commitfests", data) - self.assertIsInstance(data["commitfests"], dict) - - def test_response_content_type(self): - """Test that the response has correct Content-Type header.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - self.assertEqual(response["Content-Type"], "application/json") - - def test_cors_header_present(self): - """Test that CORS header is present for API access.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - self.assertEqual(response["Access-Control-Allow-Origin"], "*") - - def test_includes_open_commitfest(self): - """Test that open commitfests are included in response.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - commitfests = data["commitfests"] - - # Should include the open commitfest - self.assertIn("open", commitfests) - self.assertEqual(commitfests["open"]["name"], self.open_cf.name) - - def test_includes_in_progress_commitfest(self): - """Test that in-progress commitfests are included in response.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - commitfests = data["commitfests"] - - # Should include the in-progress commitfest - self.assertEqual(commitfests["in_progress"]["name"], self.in_progress_cf.name) - - def test_includes_recent_previous_commitfest(self): - """Test that recently ended commitfests are included (within 7 days).""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - commitfests = data["commitfests"] - - # Should include recent previous commitfest (ended 3 days ago) - self.assertIsNotNone(commitfests["previous"]) - - def test_excludes_old_previous_commitfest(self): - """Test that old commitfests are excluded (older than 7 days).""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - commitfests = data["commitfests"] - - # Should not include old previous commitfest (ended 10 days ago) - self.assertNotEqual( - commitfests["previous"]["name"], - self.old_previous_cf.name, - "Old previous commitfest should be excluded", - ) - - def test_excludes_next_open_and_final(self): - """Test that next_open and final are excluded from response.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - commitfests = data["commitfests"] - - # These keys should not be present in the response - self.assertNotIn("next_open", commitfests) - self.assertNotIn("final", commitfests) - - def test_response_structure(self): - """Test that response has expected structure.""" - response = self.client.get("/api/v1/commitfests/needs_ci") - data = json.loads(response.content) - - # Top-level structure - self.assertIn("commitfests", data) - self.assertIsInstance(data["commitfests"], dict) - - # Check that commitfest objects have expected fields - commitfests = data["commitfests"] - for key, cf_data in commitfests.items(): - self.assertIsInstance(cf_data, dict) - # Basic fields that should be present - self.assertIn("id", cf_data) - self.assertIn("name", cf_data) - self.assertIn("status", cf_data) - self.assertIn("startdate", cf_data) - self.assertIn("enddate", cf_data) + ), + } + + +def test_needs_ci_endpoint(client, commitfests): + """Test the /api/v1/commitfests/needs_ci endpoint returns correct data.""" + with override_settings(AUTO_CREATE_COMMITFESTS=False): + response = client.get("/api/v1/commitfests/needs_ci") + + # Check response metadata + assert response.status_code == 200 + assert response["Content-Type"] == "application/json" + assert response["Access-Control-Allow-Origin"] == "*" + + # Parse and compare response + data = json.loads(response.content) + + expected = { + "commitfests": { + "open": { + "id": commitfests["open"].id, + "name": "2025-01", + "status": "Open", + "startdate": "2025-01-01", + "enddate": "2025-01-31", + }, + "in_progress": { + "id": commitfests["in_progress"].id, + "name": "2024-11", + "status": "In Progress", + "startdate": "2024-11-01", + "enddate": "2024-11-30", + }, + "draft": { + "id": commitfests["draft"].id, + "name": "2025-03-draft", + "status": "Open", + "startdate": "2025-03-01", + "enddate": "2025-03-31", + }, + } + } + + assert data == expected diff --git a/pyproject.toml b/pyproject.toml index 3ef0d3bb..d3fd18ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ dev = [ "pycodestyle", "ruff", "djhtml", + "pytest", + "pytest-django", ] [tool.setuptools.packages.find] @@ -47,3 +49,13 @@ section-order = [ [tool.ruff.lint.isort.sections] # Group all Django imports into a separate section. django = ["django"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "pgcommitfest.settings" +python_files = ["tests.py", "test_*.py", "*_tests.py"] +testpaths = ["pgcommitfest"] +addopts = [ + "--reuse-db", + "--strict-markers", + "-vv", +] From dfe4fb58a718e5e1fb8fedf0fe884d16f7b028eb Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 10 Nov 2025 22:43:46 +0100 Subject: [PATCH 3/6] Bump to postgres 18 in CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4537f617..6d30abfd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ jobs: services: postgres: - image: postgres:14 + image: postgres:18 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres From 558440f88bb185ac456b63dcb87c49cbf1c14827 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 10 Nov 2025 22:47:41 +0100 Subject: [PATCH 4/6] Use example local settings in CI --- .github/workflows/ci.yaml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d30abfd..93023b0a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,23 +60,7 @@ jobs: uses: astral-sh/setup-uv@v4 - name: Create CI settings - run: | - cat > pgcommitfest/local_settings.py << 'EOF' - # CI test settings - DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": "pgcommitfest", - "USER": "postgres", - "PASSWORD": "postgres", - "HOST": "localhost", - "PORT": "5432", - } - } - - # Enable debug for better error messages in CI - DEBUG = True - EOF + run: cp pgcommitfest/local_settings_example.py pgcommitfest/local_settings.py - name: Install dependencies run: uv sync From 4931065739f670caef95685f2ac55c03c1f58e3e Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 10 Nov 2025 22:49:25 +0100 Subject: [PATCH 5/6] Fix db creation --- .github/workflows/ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 93023b0a..fab3da05 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,8 +65,5 @@ jobs: - name: Install dependencies run: uv sync - - name: Run migrations - run: uv run manage.py migrate --noinput - - name: Run tests - run: uv run pytest + run: uv run pytest --create-db From 27a8d9b0f6d001dcea04949bccfdb7e9e7388f87 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Mon, 10 Nov 2025 22:51:23 +0100 Subject: [PATCH 6/6] Install dev dependencies in CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fab3da05..232eb040 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,7 +63,7 @@ jobs: run: cp pgcommitfest/local_settings_example.py pgcommitfest/local_settings.py - name: Install dependencies - run: uv sync + run: uv sync --extra dev - name: Run tests run: uv run pytest --create-db