489 lines
17 KiB
Python
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()
|
|
)
|