From c06879f117f3a7b403e1f016d132a994a0ee9938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 18:37:01 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=85=20Add=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python-app.yml | 6 ++--- Makefile | 3 +++ docker-compose-dev.yml | 8 +++++++ models.py | 15 ++++++------ requirements.txt | 1 + services.py | 1 + tests/__init__.py | 0 tests/test_models.py | 39 ++++++++++++++++++++++++++++++++ 8 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_models.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 7a7ceb0..29fe128 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -29,6 +29,6 @@ jobs: - name: Lint with pylint run: | pylint $(git ls-files '*.py') --disable=C0301,R0801,R0903,R0913,R0917,W0622,W0707 -# - name: Test with pytest -# run: | -# pytest + - name: Test with pytest + run: | + pytest diff --git a/Makefile b/Makefile index a6c5ef0..5e73657 100644 --- a/Makefile +++ b/Makefile @@ -27,3 +27,6 @@ init-pre-commit: ## Init pre-commit pre-commit clean pre-commit install pre-commit run --all-files + +tests: + docker compose -f docker-compose-dev.yml run --rm test diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 5eeb622..06be034 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -4,3 +4,11 @@ services: container_name: rage-fastapi ports: - "8000:80" + + test: + build: . + volumes: + - .:/app + environment: + - PYTHONPATH=/app + entrypoint: pytest tests/ -v --tb=short diff --git a/models.py b/models.py index 3934713..d298d8c 100644 --- a/models.py +++ b/models.py @@ -14,11 +14,11 @@ class IdAndImageLink: """ id: int = Field( description="Unique identifier for the model", - example=1, + json_schema_extra={"example": 1}, ) image_link: str = Field( description="Link to an image of the model", - example="https://example.com/model_image.png" + json_schema_extra={"example": "https://example.com/model_image.png"}, ) @@ -51,15 +51,14 @@ class PedModel: """ name: str = Field( description="Name of the ped model", - example="player_zero" ) hash: str = Field( description="Hexadecimal hash of the ped model", - example="0x92A27487" + json_schema_extra={"example": "0x92A27487"}, ) image_link: str = Field( description="Link to an image of the ped model", - example="https://example.com/ped_image.png" + json_schema_extra={"example": "https://example.com/ped_image.png"}, ) @@ -70,13 +69,13 @@ class Weapon: """ name: str = Field( description="Name of the weapon", - example="WEAPON_DAGGER", + json_schema_extra={"example": "WEAPON_DAGGER"}, ) hash: str = Field( description="Hexadecimal hash of the weapon", - example="0x92A27487", + json_schema_extra={"example": "0x92A27487"}, ) type: str = Field( description="Type of weapon (melee, handguns, smg, shotguns, assault rifles, machine guns, sniper rifles, heavy weapons, throwables, misc)", - example="melee", + json_schema_extra={"example": "melee"}, ) diff --git a/requirements.txt b/requirements.txt index 7ff6f56..be0b5ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -75,6 +75,7 @@ pydantic==2.12.5 pydantic-extra-types==2.11.0 pydantic_core==2.41.5 Pygments==2.19.2 +pytest==8.3.3 python-dateutil==2.9.0.post0 python-dotenv==1.2.1 python-json-logger==4.0.0 diff --git a/services.py b/services.py index f0283a2..6b7c282 100644 --- a/services.py +++ b/services.py @@ -23,6 +23,7 @@ def get_model(model_name: str, filters) -> Union[List[BlipModel], List[BlipColor raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No model founded") return data + def get_model_with_id(id: int = None): """ Get a model with identifier. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..867b2f7 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,39 @@ +""" +This file contains unit tests for the application models. +""" + + +from models import IdAndImageLink, BlipColor, PedModel + + +def test_id_and_image_link_creation(): + """ + Test the IdAndImageLink creation. + """ + obj = IdAndImageLink(id=42, image_link="https:/example.com/foo.png") + assert obj.id == 42 + assert obj.image_link == "https:/example.com/foo.png" + + +def test_blip_color_inherits_id_and_image_link(): + """ + Test the BlipColor creation. + """ + obj = BlipColor(id=1, image_link="https:/example.com/blip.png") + assert isinstance(obj, IdAndImageLink) + assert obj.id == 1 + assert obj.image_link == "https:/example.com/blip.png" + + +def test_ped_model_creation(): + """ + Test the PedModel creation. + """ + ped = PedModel( + name="player_zero", + hash="0x92A27487", + image_link="https://example.com/ped.png", + ) + assert ped.name == "player_zero" + assert ped.hash == "0x92A27487" + assert ped.image_link == "https://example.com/ped.png" From 3a99861cbb7fd3ba98e452af0fbe3bf41f3f7e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:31:49 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=92=9A=20Add=20pytest=20&=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python-app.yml | 20 +++++++++-- .pre-commit-config.yaml | 2 +- Makefile | 2 +- docker-compose-dev.yml | 9 ++++- requirements.txt | 2 ++ tests/test_services.py | 61 ++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/test_services.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 29fe128..f5c9e6c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + pull-requests: write jobs: build: @@ -28,7 +29,22 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with pylint run: | - pylint $(git ls-files '*.py') --disable=C0301,R0801,R0903,R0913,R0917,W0622,W0707 + pylint $(git ls-files '*.py') --disable=C0301,R0801,R0903,R0913,R0917,W0622,W0707 --ignore-patterns=test_.* - name: Test with pytest run: | - pytest + pytest tests/ \ + --cov=. \ + --cov-report=term-missing \ + --cov-report=xml:coverage.xml \ + --cov-fail-under=80 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage.xml + fail_ci_if_error: true + - name: Comment Coverage PR + uses: 5monkeys/coverage-comment@v2 + if: github.event_name == 'pull_request' + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pytest_coverage_path: ./coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96761ed..bc41d6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,4 +3,4 @@ repos: rev: v4.0.4 hooks: - id: pylint - args: ["--disable=C0301,E0401,R0801,R0903,R0913,R0917,W0622,W0707"] \ No newline at end of file + args: ["--disable=C0301,E0401,R0801,R0903,R0913,R0917,W0622,W0707", "--ignore-patterns=test_.*"] \ No newline at end of file diff --git a/Makefile b/Makefile index 5e73657..bfc55be 100644 --- a/Makefile +++ b/Makefile @@ -28,5 +28,5 @@ init-pre-commit: ## Init pre-commit pre-commit install pre-commit run --all-files -tests: +tests: ## Run tests in the Docker container docker compose -f docker-compose-dev.yml run --rm test diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 06be034..e89a405 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -11,4 +11,11 @@ services: - .:/app environment: - PYTHONPATH=/app - entrypoint: pytest tests/ -v --tb=short + entrypoint: | + bash -c " + pip install coverage pytest pytest-cov && + coverage run --source=. -m pytest tests/ -v && + coverage report && + coverage html && + echo 'HTML Report generated at htmlcov/index.html' + " diff --git a/requirements.txt b/requirements.txt index be0b5ac..7524914 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ cffi==2.0.0 charset-normalizer==3.4.4 colorama==0.4.6 comm==0.2.3 +coverage==7.6.1 debugpy==1.8.17 decorator==5.2.1 defusedxml==0.7.1 @@ -76,6 +77,7 @@ pydantic-extra-types==2.11.0 pydantic_core==2.41.5 Pygments==2.19.2 pytest==8.3.3 +pytest-cov==5.0.0 python-dateutil==2.9.0.post0 python-dotenv==1.2.1 python-json-logger==4.0.0 diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..e1f7f94 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,61 @@ +""" +This file contains unit tests for the application services. +""" + +from fastapi import HTTPException +import json +import pytest +from unittest.mock import mock_open, patch + +from services import get_model + + +SAMPLE_DATA = [ + {"id": 1, "name": "A", "type": "melee"}, + {"id": 2, "name": "B", "type": "rifle"}, +] + + +def _mock_file(data): + """ + Mock the open function to return the provided data as a file-like object. + """ + return mock_open(read_data=json.dumps(data)) + + +@patch("services.open", new_callable=lambda: _mock_file(SAMPLE_DATA)) +def test_get_model_no_filters(mock_file): + """ + Test get_model without filters. + """ + result = get_model("weapons", {}) + assert result == SAMPLE_DATA + + +@patch("services.open", new_callable=lambda: _mock_file(SAMPLE_DATA)) +def test_get_model_with_id_filter(mock_file): + """ + Test get_model with id filter. + """ + result = get_model("weapons", {"id": 1}) + assert result == [{"id": 1, "name": "A", "type": "melee"}] + + +@patch("services.open", new_callable=lambda: _mock_file(SAMPLE_DATA)) +def test_get_model_with_type_filter(mock_file): + """ + Test get_model with type filter. + """ + result = get_model("weapons", {"type": "rifle"}) + assert result == [{"id": 2, "name": "B", "type": "rifle"}] + + +# @patch("services.open", side_effect=FileNotFoundError) +# def test_get_model_file_not_found(mock_file): +# """ +# Test get_model when the file is not found. +# """ +# with pytest.raises(HTTPException) as exc: +# get_model("unknown", {}) +# assert exc.value.status_code == 404 +# assert exc.value.detail == "No model founded" From b05c3e0375c33382305a824df6d795a068c8d7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:36:13 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=92=9A=20Remove=20comment=20PR=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python-app.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index f5c9e6c..9a863ce 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -42,9 +42,3 @@ jobs: with: files: ./coverage.xml fail_ci_if_error: true - - name: Comment Coverage PR - uses: 5monkeys/coverage-comment@v2 - if: github.event_name == 'pull_request' - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - pytest_coverage_path: ./coverage From 1f26d9f431c6266c6b5dfafaf5f8a0d1e726f4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:45:20 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Add=20d?= =?UTF-8?q?evelopment=20service=20to=20get=20detailed=20coverage=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-dev.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index e89a405..e584f16 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -4,7 +4,16 @@ services: container_name: rage-fastapi ports: - "8000:80" - + + coverage-report: + image: nginx:alpine + ports: + - "8080:80" # ← localhost:8080 pour voir les rapports + volumes: + - ./htmlcov:/usr/share/nginx/html:ro # monte htmlcov statique + depends_on: + - test + test: build: . volumes: From 8f7bcd7edecb1d7c7cb0aebea960921f6245d028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:15:21 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=85=20Add=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_services.py | 136 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 10 deletions(-) diff --git a/tests/test_services.py b/tests/test_services.py index e1f7f94..b8b7c2b 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -7,7 +7,18 @@ import pytest from unittest.mock import mock_open, patch -from services import get_model +from services import ( + get_model, + get_model_with_id, + get_model_with_name, + get_model_with_hash, + get_model_with_type, + get_ped_models, + get_blip_colors, + get_blip_models, + get_markers, + get_weapons, +) SAMPLE_DATA = [ @@ -50,12 +61,117 @@ def test_get_model_with_type_filter(mock_file): assert result == [{"id": 2, "name": "B", "type": "rifle"}] -# @patch("services.open", side_effect=FileNotFoundError) -# def test_get_model_file_not_found(mock_file): -# """ -# Test get_model when the file is not found. -# """ -# with pytest.raises(HTTPException) as exc: -# get_model("unknown", {}) -# assert exc.value.status_code == 404 -# assert exc.value.detail == "No model founded" +@patch("services.open", side_effect=FileNotFoundError) +def test_get_model_file_not_found(mock_file): + """ + Test get_model when the file is not found. + """ + with pytest.raises(HTTPException) as exc: + get_model("weapons", {"id": 999}) + assert exc.value.status_code == 404 + assert exc.value.detail == "No model founded" + + +def test_get_model_with_id(): + """ + Test get_model_with_id function. + """ + assert get_model_with_id(1) == {"id": 1} + assert get_model_with_id() == {} + + +def test_get_model_with_name(): + """ + Test get_model_with_name function. + """ + assert get_model_with_name("A") == {"name": "A"} + assert get_model_with_name() == {} + + +def test_get_model_with_hash(): + """ + Test get_model_with_hash function. + """ + assert get_model_with_hash("0x123") == {"hash": "0x123"} + assert get_model_with_hash() == {} + + +def test_get_model_with_type(): + """ + Test get_model_with_type function. + """ + assert get_model_with_type("melee") == {"type": "melee"} + assert get_model_with_type() == {} + + +@patch("services.get_model") +def test_get_ped_models(mock_get_model): + """Test get_ped_models merge name_filter and hash_filter.""" + name_filter = get_model_with_name("A") + hash_filter = get_model_with_hash("0x123") + mock_get_model.return_value = [{"name": "A", "hash": "0x123"}] + result = get_ped_models(name_filter=name_filter, hash_filter=hash_filter) + mock_get_model.assert_called_once_with( + "ped_models", + {"name": "A", "hash": "0x123"} + ) + assert isinstance(result, list) + +@patch("services.get_model") +def test_get_blip_colors(mock_get_model): + """Test get_blip_colors appelle get_model avec bon filename.""" + filters = get_model_with_id(1) # {"id": 1} + mock_get_model.return_value = [{"id": 1}] + + result = get_blip_colors(filters) + + mock_get_model.assert_called_once_with("blip_colors", filters) + assert isinstance(result, list) + +@patch("services.get_model") +def test_get_blip_models(mock_get_model): + """Test get_blip_models (ATTENTION: bug dans ton code → 'blip_colors' au lieu de 'blip_models' ?).""" + filters = get_model_with_id(1) + mock_get_model.return_value = [{"id": 1}] + + result = get_blip_models(filters) + + mock_get_model.assert_called_once_with("blip_models", filters) # ✅ corrige le bug si besoin + assert isinstance(result, list) + +@patch("services.get_model") +def test_get_markers(mock_get_model): + filters = get_model_with_id(1) + mock_get_model.return_value = [{"id": 1}] + + result = get_markers(filters) + + mock_get_model.assert_called_once_with("markers", filters) + assert isinstance(result, list) + +@patch("services.get_model") +def test_get_weapons(mock_get_model): + """Test get_weapons merge 3 filtres.""" + name_filter = get_model_with_name("A") + hash_filter = get_model_with_hash("0x123") + type_filter = get_model_with_type("melee") + + mock_get_model.return_value = [{"name": "A", "type": "melee"}] + + result = get_weapons( + name_filter=name_filter, + hash_filter=hash_filter, + type_filter=type_filter + ) + + # VÉRIFIE le merge des 3 dicts + mock_get_model.assert_called_once_with( + "weapons", + { + **name_filter, + **hash_filter, + **type_filter + } + ) + assert isinstance(result, list) + From 7da3b3c34215a3b4d5a1a83e10e1f47d788f17c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:16:59 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=85=20Add=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services.py | 2 +- tests/test_services.py | 23 ++++++----------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/services.py b/services.py index 6b7c282..c32e29b 100644 --- a/services.py +++ b/services.py @@ -75,7 +75,7 @@ def get_blip_models(filters = Depends(get_model_with_id)) -> List[BlipModel]: """ Get filtered or not blip models. """ - return get_model("blip_colors", filters) + return get_model("blip_models", filters) def get_markers(filters = Depends(get_model_with_id)) -> List[Marker]: diff --git a/tests/test_services.py b/tests/test_services.py index b8b7c2b..ae08981 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -106,7 +106,6 @@ def test_get_model_with_type(): @patch("services.get_model") def test_get_ped_models(mock_get_model): - """Test get_ped_models merge name_filter and hash_filter.""" name_filter = get_model_with_name("A") hash_filter = get_model_with_hash("0x123") mock_get_model.return_value = [{"name": "A", "hash": "0x123"}] @@ -117,54 +116,45 @@ def test_get_ped_models(mock_get_model): ) assert isinstance(result, list) + @patch("services.get_model") def test_get_blip_colors(mock_get_model): - """Test get_blip_colors appelle get_model avec bon filename.""" - filters = get_model_with_id(1) # {"id": 1} + filters = get_model_with_id(1) mock_get_model.return_value = [{"id": 1}] - result = get_blip_colors(filters) - mock_get_model.assert_called_once_with("blip_colors", filters) assert isinstance(result, list) + @patch("services.get_model") def test_get_blip_models(mock_get_model): - """Test get_blip_models (ATTENTION: bug dans ton code → 'blip_colors' au lieu de 'blip_models' ?).""" filters = get_model_with_id(1) mock_get_model.return_value = [{"id": 1}] - result = get_blip_models(filters) - - mock_get_model.assert_called_once_with("blip_models", filters) # ✅ corrige le bug si besoin + mock_get_model.assert_called_once_with("blip_models", filters) assert isinstance(result, list) + @patch("services.get_model") def test_get_markers(mock_get_model): filters = get_model_with_id(1) mock_get_model.return_value = [{"id": 1}] - result = get_markers(filters) - mock_get_model.assert_called_once_with("markers", filters) assert isinstance(result, list) + @patch("services.get_model") def test_get_weapons(mock_get_model): - """Test get_weapons merge 3 filtres.""" name_filter = get_model_with_name("A") hash_filter = get_model_with_hash("0x123") type_filter = get_model_with_type("melee") - mock_get_model.return_value = [{"name": "A", "type": "melee"}] - result = get_weapons( name_filter=name_filter, hash_filter=hash_filter, type_filter=type_filter ) - - # VÉRIFIE le merge des 3 dicts mock_get_model.assert_called_once_with( "weapons", { @@ -174,4 +164,3 @@ def test_get_weapons(mock_get_model): } ) assert isinstance(result, list) - From 7ffd7b89465b8609ffafd9d8887b1413d7529303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Coze?= <96086580+SebastienCozeDev@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:19:33 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=9A=20Remove=20upload=20test=20rep?= =?UTF-8?q?ort=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/python-app.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 9a863ce..c484709 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -37,8 +37,3 @@ jobs: --cov-report=term-missing \ --cov-report=xml:coverage.xml \ --cov-fail-under=80 - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - files: ./coverage.xml - fail_ci_if_error: true