""" 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() )