HH/apps/observations/tests.py
2026-01-04 10:32:40 +03:00

489 lines
17 KiB
Python

"""
Tests for the Observations app.
Tests cover:
- Public submission (no login required)
- Tracking code uniqueness
- Internal pages require auth/permissions
- Status change logs are created
- Convert-to-action creates an action and links correctly
"""
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.test import Client, TestCase
from django.urls import reverse
from apps.organizations.models import Department, Hospital
from .models import (
Observation,
ObservationCategory,
ObservationNote,
ObservationSeverity,
ObservationStatus,
ObservationStatusLog,
)
from .services import ObservationService
User = get_user_model()
class ObservationModelTests(TestCase):
"""Tests for Observation model."""
def test_tracking_code_generated(self):
"""Test that tracking code is automatically generated."""
observation = Observation.objects.create(
description="Test observation"
)
self.assertIsNotNone(observation.tracking_code)
self.assertTrue(observation.tracking_code.startswith('OBS-'))
self.assertEqual(len(observation.tracking_code), 10) # OBS-XXXXXX
def test_tracking_code_unique(self):
"""Test that tracking codes are unique."""
obs1 = Observation.objects.create(description="Test 1")
obs2 = Observation.objects.create(description="Test 2")
self.assertNotEqual(obs1.tracking_code, obs2.tracking_code)
def test_is_anonymous_property(self):
"""Test is_anonymous property."""
# Anonymous observation
obs_anon = Observation.objects.create(description="Anonymous")
self.assertTrue(obs_anon.is_anonymous)
# Identified by staff ID
obs_staff = Observation.objects.create(
description="With staff ID",
reporter_staff_id="12345"
)
self.assertFalse(obs_staff.is_anonymous)
# Identified by name
obs_name = Observation.objects.create(
description="With name",
reporter_name="John Doe"
)
self.assertFalse(obs_name.is_anonymous)
def test_severity_color(self):
"""Test severity color method."""
observation = Observation.objects.create(
description="Test",
severity=ObservationSeverity.HIGH
)
self.assertEqual(observation.get_severity_color(), 'danger')
def test_status_color(self):
"""Test status color method."""
observation = Observation.objects.create(
description="Test",
status=ObservationStatus.NEW
)
self.assertEqual(observation.get_status_color(), 'primary')
class ObservationCategoryTests(TestCase):
"""Tests for ObservationCategory model."""
def test_category_creation(self):
"""Test category creation."""
category = ObservationCategory.objects.create(
name_en="Test Category",
name_ar="فئة اختبار"
)
self.assertEqual(str(category), "Test Category")
self.assertEqual(category.name, "Test Category")
class PublicViewTests(TestCase):
"""Tests for public views (no login required)."""
def setUp(self):
self.client = Client()
self.category = ObservationCategory.objects.create(
name_en="Test Category",
is_active=True
)
def test_public_form_accessible(self):
"""Test that public form is accessible without login."""
response = self.client.get(reverse('observations:observation_create_public'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Report an Observation')
def test_public_submission_creates_observation(self):
"""Test that public submission creates an observation."""
data = {
'description': 'This is a test observation with enough detail.',
'severity': 'medium',
'category': self.category.id,
}
response = self.client.post(
reverse('observations:observation_create_public'),
data
)
# Should redirect to success page
self.assertEqual(response.status_code, 302)
# Observation should be created
self.assertEqual(Observation.objects.count(), 1)
observation = Observation.objects.first()
self.assertEqual(observation.description, data['description'])
self.assertEqual(observation.status, ObservationStatus.NEW)
def test_public_submission_anonymous(self):
"""Test anonymous submission."""
data = {
'description': 'Anonymous observation test with details.',
'severity': 'low',
}
response = self.client.post(
reverse('observations:observation_create_public'),
data
)
self.assertEqual(response.status_code, 302)
observation = Observation.objects.first()
self.assertTrue(observation.is_anonymous)
def test_public_submission_with_reporter_info(self):
"""Test submission with reporter information."""
data = {
'description': 'Observation with reporter info and details.',
'severity': 'medium',
'reporter_staff_id': '12345',
'reporter_name': 'John Doe',
}
response = self.client.post(
reverse('observations:observation_create_public'),
data
)
self.assertEqual(response.status_code, 302)
observation = Observation.objects.first()
self.assertFalse(observation.is_anonymous)
self.assertEqual(observation.reporter_staff_id, '12345')
self.assertEqual(observation.reporter_name, 'John Doe')
def test_success_page_shows_tracking_code(self):
"""Test success page displays tracking code."""
observation = Observation.objects.create(
description="Test observation"
)
response = self.client.get(
reverse('observations:observation_submitted',
kwargs={'tracking_code': observation.tracking_code})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, observation.tracking_code)
def test_track_page_accessible(self):
"""Test tracking page is accessible."""
response = self.client.get(reverse('observations:observation_track'))
self.assertEqual(response.status_code, 200)
def test_track_observation_by_code(self):
"""Test tracking observation by code."""
observation = Observation.objects.create(
description="Test observation"
)
response = self.client.get(
reverse('observations:observation_track'),
{'tracking_code': observation.tracking_code}
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, observation.tracking_code)
def test_honeypot_blocks_spam(self):
"""Test honeypot field blocks spam submissions."""
data = {
'description': 'Spam observation with enough detail here.',
'severity': 'medium',
'website': 'spam-value', # Honeypot field
}
response = self.client.post(
reverse('observations:observation_create_public'),
data
)
# Should not create observation
self.assertEqual(Observation.objects.count(), 0)
class InternalViewTests(TestCase):
"""Tests for internal views (login required)."""
def setUp(self):
self.client = Client()
# Create hospital and department
self.hospital = Hospital.objects.create(
name="Test Hospital",
code="TH001"
)
self.department = Department.objects.create(
name="Test Department",
hospital=self.hospital,
status='active'
)
# Create users
self.admin_user = User.objects.create_user(
email='admin@test.com',
password='testpass123',
is_staff=True,
hospital=self.hospital
)
self.regular_user = User.objects.create_user(
email='user@test.com',
password='testpass123',
hospital=self.hospital
)
# Create PX Admin group and add admin user
px_admin_group, _ = Group.objects.get_or_create(name='PX Admin')
self.admin_user.groups.add(px_admin_group)
# Create observation
self.observation = Observation.objects.create(
description="Test observation for internal views"
)
def test_list_requires_login(self):
"""Test that list view requires login."""
response = self.client.get(reverse('observations:observation_list'))
self.assertEqual(response.status_code, 302)
self.assertIn('login', response.url)
def test_list_accessible_when_logged_in(self):
"""Test list view accessible when logged in."""
self.client.login(email='admin@test.com', password='testpass123')
response = self.client.get(reverse('observations:observation_list'))
self.assertEqual(response.status_code, 200)
def test_detail_requires_login(self):
"""Test that detail view requires login."""
response = self.client.get(
reverse('observations:observation_detail',
kwargs={'pk': self.observation.id})
)
self.assertEqual(response.status_code, 302)
def test_detail_accessible_when_logged_in(self):
"""Test detail view accessible when logged in."""
self.client.login(email='admin@test.com', password='testpass123')
response = self.client.get(
reverse('observations:observation_detail',
kwargs={'pk': self.observation.id})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.observation.tracking_code)
class ObservationServiceTests(TestCase):
"""Tests for ObservationService."""
def setUp(self):
self.hospital = Hospital.objects.create(
name="Test Hospital",
code="TH001"
)
self.department = Department.objects.create(
name="Test Department",
hospital=self.hospital,
status='active'
)
self.user = User.objects.create_user(
email='test@test.com',
password='testpass123',
hospital=self.hospital
)
self.category = ObservationCategory.objects.create(
name_en="Test Category",
is_active=True
)
def test_create_observation_service(self):
"""Test creating observation via service."""
observation = ObservationService.create_observation(
description="Service created observation",
severity='high',
category=self.category,
reporter_name="Test Reporter"
)
self.assertIsNotNone(observation.id)
self.assertEqual(observation.status, ObservationStatus.NEW)
self.assertFalse(observation.is_anonymous)
# Check status log was created
self.assertEqual(observation.status_logs.count(), 1)
log = observation.status_logs.first()
self.assertEqual(log.to_status, ObservationStatus.NEW)
def test_change_status_creates_log(self):
"""Test that changing status creates a log entry."""
observation = Observation.objects.create(
description="Test observation"
)
ObservationService.change_status(
observation=observation,
new_status=ObservationStatus.TRIAGED,
changed_by=self.user,
comment="Triaging this observation"
)
observation.refresh_from_db()
self.assertEqual(observation.status, ObservationStatus.TRIAGED)
self.assertIsNotNone(observation.triaged_at)
# Check log
log = observation.status_logs.filter(to_status=ObservationStatus.TRIAGED).first()
self.assertIsNotNone(log)
self.assertEqual(log.changed_by, self.user)
self.assertEqual(log.comment, "Triaging this observation")
def test_triage_observation(self):
"""Test triaging an observation."""
observation = Observation.objects.create(
description="Test observation"
)
ObservationService.triage_observation(
observation=observation,
triaged_by=self.user,
assigned_department=self.department,
assigned_to=self.user,
note="Assigning to department"
)
observation.refresh_from_db()
self.assertEqual(observation.assigned_department, self.department)
self.assertEqual(observation.assigned_to, self.user)
self.assertEqual(observation.status, ObservationStatus.ASSIGNED)
# Check note was created
self.assertTrue(observation.notes.filter(note="Assigning to department").exists())
def test_add_note(self):
"""Test adding a note to observation."""
observation = Observation.objects.create(
description="Test observation"
)
note = ObservationService.add_note(
observation=observation,
note="This is a test note",
created_by=self.user,
is_internal=True
)
self.assertIsNotNone(note.id)
self.assertEqual(note.observation, observation)
self.assertEqual(note.created_by, self.user)
self.assertTrue(note.is_internal)
class StatusLogTests(TestCase):
"""Tests for status logging."""
def setUp(self):
self.user = User.objects.create_user(
email='test@test.com',
password='testpass123'
)
def test_status_log_created_on_status_change(self):
"""Test that status log is created when status changes."""
observation = Observation.objects.create(
description="Test observation"
)
# Change status
ObservationService.change_status(
observation=observation,
new_status=ObservationStatus.IN_PROGRESS,
changed_by=self.user
)
# Check log exists
logs = ObservationStatusLog.objects.filter(observation=observation)
self.assertEqual(logs.count(), 1)
log = logs.first()
self.assertEqual(log.from_status, ObservationStatus.NEW)
self.assertEqual(log.to_status, ObservationStatus.IN_PROGRESS)
self.assertEqual(log.changed_by, self.user)
def test_multiple_status_changes_logged(self):
"""Test that multiple status changes are all logged."""
observation = Observation.objects.create(
description="Test observation"
)
# Multiple status changes
statuses = [
ObservationStatus.TRIAGED,
ObservationStatus.ASSIGNED,
ObservationStatus.IN_PROGRESS,
ObservationStatus.RESOLVED,
]
for status in statuses:
ObservationService.change_status(
observation=observation,
new_status=status,
changed_by=self.user
)
# Check all logs exist
logs = ObservationStatusLog.objects.filter(observation=observation)
self.assertEqual(logs.count(), len(statuses))
class CategoryManagementTests(TestCase):
"""Tests for category management."""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
email='admin@test.com',
password='testpass123',
is_staff=True
)
# Add manage_categories permission
permission = Permission.objects.get(codename='manage_categories')
self.user.user_permissions.add(permission)
def test_category_list_requires_permission(self):
"""Test category list requires permission."""
self.client.login(email='admin@test.com', password='testpass123')
response = self.client.get(reverse('observations:category_list'))
self.assertEqual(response.status_code, 200)
def test_category_create(self):
"""Test creating a category."""
self.client.login(email='admin@test.com', password='testpass123')
data = {
'name_en': 'New Category',
'name_ar': 'فئة جديدة',
'sort_order': 1,
'is_active': True,
}
response = self.client.post(
reverse('observations:category_create'),
data
)
self.assertEqual(response.status_code, 302)
self.assertTrue(
ObservationCategory.objects.filter(name_en='New Category').exists()
)