diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 8579a2d..bbd5b0f 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -79,6 +79,7 @@ jobs: - name: Test with pytest run: | uv run pytest --cov-report xml:coverage.xml --junitxml=pytest.xml --cov-fail-under=80 + continue-on-error: true # Always continue to upload reports - name: Upload coverage and test reports # Upload coverage report only once to avoid redundancy @@ -100,6 +101,19 @@ jobs: # No shallow clone for a better analysis fetch-depth: 0 + - name: Set up Python 3.13 + uses: actions/setup-python@v3 + with: + python-version: 3.13 + - name: Install Mypy + run: | + python -m pip install mypy django-stubs types-pywin32 + + - name: Run MyPy + run: | + mypy --strict --junit-xml mypy.xml src + continue-on-error: true + - name: Download coverage report uses: actions/download-artifact@v4 with: @@ -110,4 +124,9 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - url: ${{ secrets.SONAR_HOST_URL }}/dashboard?id=django-windowsauthtoken + + - name: SonarQube Quality Gate check + uses: sonarsource/sonarqube-quality-gate-action@v1.2.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/pyproject.toml b/pyproject.toml index 06dc38d..4e78c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,6 @@ source = ["django_windowsauthtoken"] [tool.pytest.ini_options] addopts = "--cov --cov-report=term-missing" + +[tool.mypy] +strict = true diff --git a/sonar-project.properties b/sonar-project.properties index bba5c25..947b2fe 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,3 +5,4 @@ sonar.sources = src sonar.tests = tests sonar.python.coverage.reportPaths=coverage.xml sonar.python.xunit.reportPath=pytest.xml +sonar.python.mypy.reportPaths=mypy.xml diff --git a/src/django_windowsauthtoken/middleware.py b/src/django_windowsauthtoken/middleware.py index 99d272f..82e9404 100644 --- a/src/django_windowsauthtoken/middleware.py +++ b/src/django_windowsauthtoken/middleware.py @@ -1,8 +1,10 @@ import logging import os +from typing import Callable from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.http import HttpRequest, HttpResponse from django.utils.module_loading import import_string from .formatters import DEFAULT_FORMATTER, FormattingError @@ -16,12 +18,12 @@ import pywintypes import win32api import win32security -except ImportError: - if _IGNORE_PYWIN32_ERRORS: # pragma: no cover +except ImportError: # pragma: no cover + if _IGNORE_PYWIN32_ERRORS: logger.warning("pywin32 is not installed, but errors are being ignored.") - pywintypes = None - win32api = None - win32security = None + pywintypes = None # type: ignore[assignment] + win32api = None # type: ignore[assignment] + win32security = None # type: ignore[assignment] class WindowsAuthTokenMiddleware: @@ -34,14 +36,14 @@ class WindowsAuthTokenMiddleware: header_name = "X-IIS-WindowsAuthToken" """The HTTP header name where the Windows Authentication Token is expected.""" - def __init__(self, get_response): + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: self.get_response = get_response self.username_formatter: str = getattr(settings, "WINDOWSAUTHTOKEN_USERNAME_FORMATTER", DEFAULT_FORMATTER) if not any([win32security, pywintypes, win32api]) and not _IGNORE_PYWIN32_ERRORS: raise ImproperlyConfigured("pywin32 is required for Windows Authentication Token middleware.'") - def __call__(self, request): + def __call__(self, request: HttpRequest) -> HttpResponse: auth_token = request.headers.get(self.header_name, "") if auth_token: try: @@ -135,5 +137,5 @@ def format_username(self, user: str, domain: str) -> str: Raises: FormattingError: If the formatter raises an error. """ - formatter = import_string(self.username_formatter) + formatter: Callable[[str, str], str] = import_string(self.username_formatter) return formatter(user, domain) diff --git a/src/django_windowsauthtoken/views.py b/src/django_windowsauthtoken/views.py index 6320418..4682444 100644 --- a/src/django_windowsauthtoken/views.py +++ b/src/django_windowsauthtoken/views.py @@ -1,10 +1,10 @@ from django.conf import settings -from django.http import JsonResponse +from django.http import HttpRequest, JsonResponse from django.views.decorators.http import require_GET @require_GET -def debug_view(request): +def debug_view(request: HttpRequest) -> JsonResponse: """ A simple debug view that returns the current user's username and domain. """ @@ -22,7 +22,7 @@ def debug_view(request): # State of the user object "user.is_authenticated": request.user.is_authenticated, "user.is_anonymous": request.user.is_anonymous, - "user.username": request.user.username if request.user.is_authenticated else "N/A", + "user.username": request.user.get_username() if request.user.is_authenticated else "N/A", # Full request representation for deeper debugging if needed, mainly ASGI/WSGI differences "request": str(request), # Full META dump for deeper debugging if needed diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 47eec4b..5ddf0f8 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -270,4 +270,4 @@ async def test_combine_with_remote_user_middleware_async(mocker, settings, async assert await User.objects.acount() == 1, "User should be created by RemoteUserMiddleware" user = await User.objects.afirst() assert user == response.asgi_request.user - assert user.username == r"TESTDOMAIN\testuser" + assert user.get_username() == r"TESTDOMAIN\testuser"