- 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.
190 lines
6.7 KiB
Python
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
|
|
)
|