bottleneckcrm/backend/common/tests/test_pat_integration.py
Ashwin bd79a7a4cc Refactor code style and improve error handling in analytics API
- Cleaned up admin.py by removing unnecessary dictionary wrapping for fields.
- Adjusted migration files to maintain consistent formatting and removed redundant lines.
- Updated models.py and tests to remove unnecessary blank lines and improve readability.
- Enhanced error handling in the analytics API to properly forward upstream status and messages.
- Added comprehensive tests for the analytics proxy to ensure correct status and body forwarding.
- Updated contributing guidelines to clarify the use of ruff for code style enforcement.
2026-08-07 15:58:35 +05:30

65 lines
3.1 KiB
Python

"""Integration tests driving a PAT through the REAL middleware stack.
These use Django's test ``Client`` (not DRF's APIClient with forced auth) so the
request passes through GetProfileAndOrg → RequireOrgContext → DRF auth exactly
as in production. They run on SQLite (no postgres_only marker) and are the
regression guard for the bug where a valid PAT was rejected with 403 by
RequireOrgContext because org context was never set in middleware.
These probes used to point at ``/api/profile/tokens/``. That endpoint is now on
the credential deny-list in ``common.scopes``: a token may not read or mint
another token. The probes moved to an ordinary endpoint, which is what they were
always really testing, and the deny is asserted here too so the reason this file
changed is recorded next to it rather than only in a commit message.
"""
import pytest
from django.test import Client
from django.utils import timezone
from common.models import PersonalAccessToken
@pytest.mark.django_db
class TestPATThroughMiddlewareStack:
PROBE_URL = "/api/leads/"
TOKEN_URL = "/api/profile/tokens/"
def test_pat_request_reaches_view(self, admin_profile):
"""A valid PAT must reach a protected view (200), NOT be 403'd by
RequireOrgContext. This is the core regression guard for the fix."""
raw, _ = PersonalAccessToken.generate(profile=admin_profile, name="cli")
client = Client()
resp = client.get(self.PROBE_URL, HTTP_AUTHORIZATION=f"Bearer {raw}")
assert resp.status_code == 200, resp.content
def test_pat_can_list_contacts_via_middleware(self, admin_profile):
"""Same PAT reaching a second protected view."""
raw, _ = PersonalAccessToken.generate(profile=admin_profile, name="cli")
client = Client()
resp = client.get("/api/contacts/", HTTP_AUTHORIZATION=f"Bearer {raw}")
assert resp.status_code == 200, resp.content
def test_pat_cannot_reach_the_token_endpoint(self, admin_profile):
"""The deny that replaced the old probe. A token cannot manage tokens."""
raw, _ = PersonalAccessToken.generate(profile=admin_profile, name="cli")
client = Client()
resp = client.get(self.TOKEN_URL, HTTP_AUTHORIZATION=f"Bearer {raw}")
assert resp.status_code == 403, resp.content
def test_invalid_pat_is_denied(self, admin_profile):
"""A bogus PAT must be denied (not 500, not 200)."""
client = Client()
resp = client.get(self.PROBE_URL, HTTP_AUTHORIZATION="Bearer bcrm_pat_bogus")
assert resp.status_code in (401, 403), resp.content
assert resp.status_code not in (200, 500)
def test_revoked_pat_is_denied_through_stack(self, admin_profile):
"""A revoked PAT must be denied through the full stack."""
raw, pat = PersonalAccessToken.generate(profile=admin_profile, name="cli")
pat.revoked_at = timezone.now()
pat.save(update_fields=["revoked_at"])
client = Client()
resp = client.get(self.PROBE_URL, HTTP_AUTHORIZATION=f"Bearer {raw}")
assert resp.status_code in (401, 403), resp.content
assert resp.status_code not in (200, 500)