bottleneckcrm/backend/common/tests/test_user_views_org_context.py
Ashwin ef9157f163 Refactor API views to improve parameter handling and validation
- Replaced direct usage of `json.loads` for parsing parameters with dedicated validation functions (`payload_id_list`, `uuid_list_param`, `uuid_param`) across multiple views to enhance error handling and maintainability.
- Updated filtering logic in various views to ensure proper UUID handling for related entities (e.g., `assigned_to`, `teams`, `contacts`, etc.).
- Enhanced permission checks by adding `HasOrgContext` to user-related views to enforce organization context.
- Improved error handling in invoice creation tests to correctly assert permission denial responses.
- Streamlined query parameter processing in Kanban and list views to reduce redundancy and improve clarity.
2026-08-05 05:09:11 +05:30

99 lines
3.8 KiB
Python

"""``HasOrgContext`` on the three user-management views.
``UsersListView``, ``UserDetailView`` and ``UserStatusView`` each read
``request.profile`` unconditionally: ``request.profile.role`` for the admin
check, ``request.profile.org`` for the object lookup. ``GetProfileAndOrg``
leaves ``request.profile`` as ``None`` whenever the JWT carries no ``org_id``
claim, or carries one the caller is no longer a member of, so every one of
those reads was an ``AttributeError`` on a stale token: a 500 where the answer
should have been a 403.
This is not a tenant leak. The middleware already requires ``is_active=True``
before it sets ``request.profile`` (``common/middleware/get_company.py``), so a
deactivated profile never reached these views to begin with. What was missing
was the clean refusal.
Both directions are asserted, since a permission class that cannot return
``False`` is decoration and one that cannot return ``True`` is an outage.
"""
import pytest
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from common.models import Profile
def _client_without_org_claim(user):
"""A client whose JWT authenticates but names no org.
This is the shape a token has between login and picking an org, and the
shape a stale token keeps after its membership is revoked.
"""
client = APIClient()
token = RefreshToken.for_user(user)
client.credentials(HTTP_AUTHORIZATION=f"Bearer {token.access_token}")
return client
@pytest.mark.django_db
class TestUsersListViewOrgContext:
url = "/api/users/"
def test_without_org_context_it_is_refused_not_crashed(self, admin_user):
response = _client_without_org_claim(admin_user).get(self.url)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_with_org_context_an_admin_still_gets_through(self, admin_client):
"""The False direction is worthless without this one beside it."""
response = admin_client.get(self.url)
assert response.status_code == status.HTTP_200_OK
def test_post_without_org_context_is_refused(self, admin_user):
response = _client_without_org_claim(admin_user).post(
self.url, {"email": "x@test.com", "role": "USER"}, format="json"
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.django_db
class TestUserDetailViewOrgContext:
def test_without_org_context_it_is_refused_not_crashed(
self, admin_user, admin_profile
):
client = _client_without_org_claim(admin_user)
response = client.get(f"/api/user/{admin_user.id}/")
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_with_org_context_an_admin_still_gets_through(
self, admin_client, admin_user, admin_profile
):
response = admin_client.get(f"/api/user/{admin_user.id}/")
assert response.status_code == status.HTTP_200_OK
@pytest.mark.django_db
class TestUserStatusViewOrgContext:
def test_without_org_context_it_is_refused_not_crashed(
self, admin_user, user_profile
):
client = _client_without_org_claim(admin_user)
response = client.post(
f"/api/user/{user_profile.user.id}/status/",
{"status": "Inactive"},
format="json",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_with_org_context_an_admin_still_gets_through(
self, admin_client, user_profile
):
response = admin_client.post(
f"/api/user/{user_profile.user.id}/status/",
{"status": "Inactive"},
format="json",
)
assert response.status_code == status.HTTP_200_OK
user_profile.refresh_from_db()
assert Profile.objects.get(pk=user_profile.pk).is_active is False