bottleneckcrm/backend/cases/tests/test_internal_notes.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

190 lines
6.7 KiB
Python

"""
Tests for the Tier-1 internal-notes feature: Comment.is_internal split,
permission rule, and audit-log on visibility flips.
See docs/cases/tier1/internal-notes.md.
"""
from __future__ import annotations
import pytest
from django.contrib.contenttypes.models import ContentType
from common.models import Activity, Comment
def _make_comment(case, profile, *, text="hi", is_internal=False):
return Comment.objects.create(
comment=text,
content_type=ContentType.objects.get_for_model(case),
object_id=case.pk,
commented_by=profile,
is_internal=is_internal,
org=case.org,
)
@pytest.mark.django_db
class TestCommentSplitResponse:
def test_get_splits_public_and_internal(self, admin_client, case_a, admin_profile):
_make_comment(case_a, admin_profile, text="public reply", is_internal=False)
_make_comment(case_a, admin_profile, text="agent only", is_internal=True)
response = admin_client.get(f"/api/cases/{case_a.pk}/")
assert response.status_code == 200
body = response.json()
assert "comments" in body and "internal_notes" in body
assert {c["comment"] for c in body["comments"]} == {"public reply"}
assert {c["comment"] for c in body["internal_notes"]} == {"agent only"}
# Each comment payload exposes is_internal so the frontend can render
# mixed lists if needed.
assert all(c["is_internal"] is False for c in body["comments"])
assert all(c["is_internal"] is True for c in body["internal_notes"])
@pytest.mark.django_db
class TestPostInternalComment:
def test_admin_can_post_internal_note(self, admin_client, case_a):
response = admin_client.post(
f"/api/cases/{case_a.pk}/",
{"comment": "engineering ack pending", "is_internal": True},
format="json",
)
assert response.status_code == 200, response.content
body = response.json()
# Returns split arrays; the new note shows up under internal_notes.
assert any(
c["comment"] == "engineering ack pending" and c["is_internal"] is True
for c in body["internal_notes"]
)
# And it's not in the public bucket.
assert not any(
c["comment"] == "engineering ack pending" for c in body["comments"]
)
def test_default_is_public_when_not_provided(self, admin_client, case_a):
response = admin_client.post(
f"/api/cases/{case_a.pk}/",
{"comment": "default comment"},
format="json",
)
assert response.status_code == 200, response.content
body = response.json()
assert any(
c["comment"] == "default comment" and c["is_internal"] is False
for c in body["comments"]
)
def test_is_internal_string_truthy_coerced(self, admin_client, case_a):
# The comment-attachment endpoint accepts multipart for file uploads,
# so flags arrive as strings. Make sure "true" is coerced to bool True.
response = admin_client.post(
f"/api/cases/{case_a.pk}/",
{"comment": "string-truthy", "is_internal": "true"},
format="multipart",
)
assert response.status_code == 200, response.content
body = response.json()
assert any(
c["comment"] == "string-truthy" and c["is_internal"] is True
for c in body["internal_notes"]
)
@pytest.mark.django_db
class TestCrossOrgIsolation:
def test_other_org_cannot_see_internal_notes(
self, org_b_client, case_a, admin_profile
):
_make_comment(case_a, admin_profile, text="leak?", is_internal=True)
# Org B should not be able to fetch this case at all.
response = org_b_client.get(f"/api/cases/{case_a.pk}/")
assert response.status_code in (403, 404)
@pytest.mark.django_db
class TestVisibilityFlipAuditLogged:
def test_flip_public_to_internal_emits_comment_activity(
self, admin_client, case_a, admin_profile
):
comment = _make_comment(
case_a, admin_profile, text="leaked?", is_internal=False
)
# Baseline: one COMMENT activity row for creation.
baseline_count = Activity.objects.filter(
entity_type="Case", entity_id=case_a.pk, action="COMMENT"
).count()
response = admin_client.patch(
f"/api/cases/comment/{comment.pk}/",
{"is_internal": True},
format="json",
)
assert response.status_code == 200, response.content
flip_rows = list(
Activity.objects.filter(
entity_type="Case",
entity_id=case_a.pk,
action="COMMENT",
).order_by("created_at")
)
assert len(flip_rows) == baseline_count + 1
latest = flip_rows[-1]
meta = latest.metadata
assert meta.get("visibility_changed") is True
assert meta.get("before") is False
assert meta.get("after") is True
assert meta.get("comment_id") == str(comment.pk)
def test_flip_internal_to_public_emits_comment_activity(
self, admin_client, case_a, admin_profile
):
comment = _make_comment(
case_a, admin_profile, text="oops private", is_internal=True
)
response = admin_client.patch(
f"/api/cases/comment/{comment.pk}/",
{"is_internal": False},
format="json",
)
assert response.status_code == 200, response.content
latest = (
Activity.objects.filter(
entity_type="Case",
entity_id=case_a.pk,
action="COMMENT",
)
.order_by("-created_at")
.first()
)
assert latest is not None
meta = latest.metadata
assert meta.get("visibility_changed") is True
assert meta.get("before") is True
assert meta.get("after") is False
def test_text_only_edit_does_not_emit_flip(
self, admin_client, case_a, admin_profile
):
comment = _make_comment(
case_a, admin_profile, text="original", is_internal=False
)
baseline_count = Activity.objects.filter(
entity_type="Case", entity_id=case_a.pk, action="COMMENT"
).count()
response = admin_client.patch(
f"/api/cases/comment/{comment.pk}/",
{"comment": "edited text"},
format="json",
)
assert response.status_code == 200, response.content
# No new activity row.
assert (
Activity.objects.filter(
entity_type="Case", entity_id=case_a.pk, action="COMMENT"
).count()
== baseline_count
)