from django.contrib.auth.models import User from django.test import TestCase, Client from django.urls import reverse from common.models import UserConfig, Thematic, UserThematics, AppView class SignViewTests(TestCase): """Tests for the sign view functionality.""" def setUp(self): """Set up test data.""" self.client = Client() # Create application views self.asset_view = AppView.objects.create( code='asset_management', name_fr='Gestion des assets', name_nl='Asset Management', icon='bi-database', url_name='home:index', is_default=True, order=1 ) self.sign_view = AppView.objects.create( code='sign', name_fr='Signalisation', name_nl='Signalisatie', icon='bi-signpost', url_name='sign:index', order=2 ) # Create a thematic for sign self.sign_thematic = Thematic.objects.create( code='sign', name_fr='Signalisation', name_nl='Signalisatie', icon='bi-signpost', color='#0022ff' ) # Create a user with access to sign view self.user_with_access = User.objects.create_user( username='signuser', password='testpass123' ) self.config_with_access = UserConfig.objects.create( user=self.user_with_access ) self.config_with_access.accessible_views.add(self.asset_view, self.sign_view) # Give the user access to sign thematic UserThematics.objects.create( user_config=self.config_with_access, thematic=self.sign_thematic, can_view_assets=True, can_edit_assets=False ) # Create a user without access to sign view self.user_without_access = User.objects.create_user( username='notsignuser', password='testpass123' ) self.config_without_access = UserConfig.objects.create( user=self.user_without_access ) self.config_without_access.accessible_views.add(self.asset_view) # Create a user with only sign access self.user_sign_only = User.objects.create_user( username='signonly', password='testpass123' ) self.config_sign_only = UserConfig.objects.create( user=self.user_sign_only ) self.config_sign_only.accessible_views.add(self.sign_view) UserThematics.objects.create( user_config=self.config_sign_only, thematic=self.sign_thematic, can_view_assets=True ) def test_sign_index_access_granted(self): """Test that users with sign access can view the page.""" self.client.force_login(self.user_with_access) response = self.client.get(reverse('sign:index')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'sign/index.html') def test_sign_index_access_denied(self): """Test that users without sign access get 403 Forbidden.""" self.client.force_login(self.user_without_access) response = self.client.get(reverse('sign:index')) self.assertEqual(response.status_code, 403) def test_sign_requires_authentication(self): """Test that unauthenticated users are redirected to login.""" response = self.client.get(reverse('sign:index')) self.assertRedirects( response, f"{reverse('login')}?next={reverse('sign:index')}" ) def test_sign_index_contains_required_elements(self): """Test that the sign page contains required UI elements.""" self.client.force_login(self.user_with_access) response = self.client.get(reverse('sign:index')) # Check for main container self.assertContains(response, 'sign-container') # Check for left panel self.assertContains(response, 'sign-panel') # Check for map container self.assertContains(response, 'sign-map') # Check for tabs self.assertContains(response, 'tasks-tab') self.assertContains(response, 'filters-tab') self.assertContains(response, 'regulations-tab') def test_sign_filter_parameters(self): """Test that filter parameters are passed to context.""" self.client.force_login(self.user_with_access) response = self.client.get( reverse('sign:index'), {'municipality': ['1', '2'], 'panel': 'filters'} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['active_panel'], 'filters') self.assertIn('1', response.context['selected_municipalities']) self.assertIn('2', response.context['selected_municipalities']) class ViewSwitcherTests(TestCase): """Tests for the view switcher functionality in the header.""" def setUp(self): """Set up test data.""" self.client = Client() # Create application views self.asset_view = AppView.objects.create( code='asset_management', name_fr='Gestion des assets', name_nl='Asset Management', icon='bi-database', url_name='home:index', is_default=True, order=1 ) self.sign_view = AppView.objects.create( code='sign', name_fr='Signalisation', name_nl='Signalisatie', icon='bi-signpost', url_name='sign:index', order=2 ) # Create users with different access levels self.user_both_views = User.objects.create_user( username='bothviews', password='testpass123' ) self.config_both = UserConfig.objects.create( user=self.user_both_views ) self.config_both.accessible_views.add(self.asset_view, self.sign_view) self.user_asset_only = User.objects.create_user( username='assetonly', password='testpass123' ) self.config_asset = UserConfig.objects.create( user=self.user_asset_only ) self.config_asset.accessible_views.add(self.asset_view) self.user_sign_only = User.objects.create_user( username='signonly', password='testpass123' ) self.config_sign = UserConfig.objects.create( user=self.user_sign_only ) self.config_sign.accessible_views.add(self.sign_view) def test_view_switcher_shown_for_both_views(self): """Test that view switcher is shown for users with access to both views.""" self.client.force_login(self.user_both_views) response = self.client.get(reverse('home:index')) self.assertEqual(response.status_code, 200) self.assertContains(response, 'viewSwitcherDropdown') def test_view_switcher_hidden_for_single_view_asset(self): """Test that view switcher dropdown is not shown for asset-only users.""" self.client.force_login(self.user_asset_only) response = self.client.get(reverse('home:index')) self.assertEqual(response.status_code, 200) # The dropdown should not be shown self.assertNotContains(response, 'viewSwitcherDropdown') class UserConfigViewAccessTests(TestCase): """Tests for UserConfig view access with generic AppView model.""" def setUp(self): """Set up test data.""" self.asset_view = AppView.objects.create( code='asset_management', name_fr='Gestion des assets', name_nl='Asset Management', icon='bi-database', url_name='home:index', is_default=True, order=1 ) self.sign_view = AppView.objects.create( code='sign', name_fr='Signalisation', name_nl='Signalisatie', icon='bi-signpost', url_name='sign:index', order=2 ) def test_can_access_view_method(self): """Test the can_access_view method.""" user = User.objects.create_user(username='testuser', password='pass') config = UserConfig.objects.create(user=user) config.accessible_views.add(self.sign_view) self.assertTrue(config.can_access_view('sign')) self.assertFalse(config.can_access_view('asset_management')) def test_can_add_multiple_views(self): """Test that multiple views can be added.""" user = User.objects.create_user(username='testuser', password='pass') config = UserConfig.objects.create(user=user) config.accessible_views.add(self.asset_view, self.sign_view) self.assertTrue(config.can_access_view('asset_management')) self.assertTrue(config.can_access_view('sign')) self.assertEqual(config.accessible_views.count(), 2) def test_get_accessible_views_ordered(self): """Test that get_accessible_views returns views in order.""" user = User.objects.create_user(username='testuser', password='pass') config = UserConfig.objects.create(user=user) config.accessible_views.add(self.sign_view, self.asset_view) views = list(config.get_accessible_views()) self.assertEqual(views[0].code, 'asset_management') # order=1 self.assertEqual(views[1].code, 'sign') # order=2 class AppViewModelTests(TestCase): """Tests for the AppView model.""" def test_app_view_creation(self): """Test creating an AppView.""" view = AppView.objects.create( code='test_view', name_fr='Vue de test', name_nl='Test view', icon='bi-test', url_name='test:index', is_default=False, order=10 ) self.assertEqual(view.code, 'test_view') self.assertEqual(view.name_fr, 'Vue de test') self.assertEqual(view.url_name, 'test:index') def test_app_view_name_property(self): """Test the name property returns the correct language.""" view = AppView.objects.create( code='test_view', name_fr='Vue de test', name_nl='Test view', icon='bi-test', url_name='test:index' ) # Default language should be French self.assertEqual(view.name, 'Vue de test') class ESignPermissionTests(TestCase): """Tests for the refined eSign permissions.""" def setUp(self): from django.contrib.auth.models import Permission from common.models import Role # Create basic roles self.admin_role, _ = Role.objects.get_or_create(name='admin') self.manager_role, _ = Role.objects.get_or_create(name='manager') # Create superuser self.superuser = User.objects.create_superuser(username='super', password='password') # Create normal users self.user_none = User.objects.create_user(username='none', password='password') self.config_none = UserConfig.objects.create(user=self.user_none) self.user_edit = User.objects.create_user(username='edit', password='password') self.config_edit = UserConfig.objects.create(user=self.user_edit, esign_can_edit=True) self.user_manage = User.objects.create_user(username='manage', password='password') self.config_manage = UserConfig.objects.create(user=self.user_manage, esign_can_manage_rcp=True) self.user_validate = User.objects.create_user(username='validate', password='password') self.config_validate = UserConfig.objects.create(user=self.user_validate, esign_can_validate_rcp=True) # Users with roles self.user_admin = User.objects.create_user(username='admin_user', password='password') self.config_admin = UserConfig.objects.create(user=self.user_admin) self.config_admin.roles.add(self.admin_role) self.user_manager = User.objects.create_user(username='manager_user', password='password') self.config_manager = UserConfig.objects.create(user=self.user_manager) self.config_manager.roles.add(self.manager_role) # User with Django built-in permission self.user_django_perm = User.objects.create_user(username='django_perm', password='password') self.config_django_perm = UserConfig.objects.create(user=self.user_django_perm) add_perm = Permission.objects.filter(codename='add_signpole', content_type__app_label='sign').first() if not add_perm: add_perm = Permission.objects.filter(codename='add_signpole').first() self.user_django_perm.user_permissions.add(add_perm) def test_user_can_edit_esign_assets(self): from sign.permissions import user_can_edit_esign_assets # Superuser should be able to edit self.assertTrue(user_can_edit_esign_assets(self.superuser)) # Normal user should not be able to edit self.assertFalse(user_can_edit_esign_assets(self.user_none)) # User with esign_can_edit should be able to edit self.assertTrue(user_can_edit_esign_assets(self.user_edit)) # User with django perm should be able to edit # Refresh user instance to load permissions user_django = User.objects.get(pk=self.user_django_perm.pk) self.assertTrue(user_can_edit_esign_assets(user_django)) def test_user_can_manage_regulations(self): from sign.permissions import user_can_manage_regulations # Superuser should be able to manage self.assertTrue(user_can_manage_regulations(self.superuser)) # Normal user should not be able to manage self.assertFalse(user_can_manage_regulations(self.user_none)) # User with esign_can_manage_rcp should be able to manage self.assertTrue(user_can_manage_regulations(self.user_manage)) # Admin and manager roles should be able to manage self.assertTrue(user_can_manage_regulations(self.user_admin)) self.assertTrue(user_can_manage_regulations(self.user_manager)) def test_user_can_approve_regulation(self): from sign.permissions import user_can_approve_regulation # Superuser should be able to approve self.assertTrue(user_can_approve_regulation(self.superuser)) # Normal user should not be able to approve self.assertFalse(user_can_approve_regulation(self.user_none)) # User with esign_can_validate_rcp should be able to approve self.assertTrue(user_can_approve_regulation(self.user_validate)) # Admin role should be able to approve self.assertTrue(user_can_approve_regulation(self.user_admin)) # Manager role should NOT be able to approve (requires admin or validate permission) self.assertFalse(user_can_approve_regulation(self.user_manager)) class ESignOrganizationRestrictionsTests(TestCase): """Tests for eSign Organization restrictions and validation.""" def setUp(self): from sign.models import Organization, ComplementaryPoliceRegulation self.client = Client() # Create basic app views so we can access 'sign' view self.sign_view = AppView.objects.create( code='sign', name_fr='Signalisation', name_nl='Signalisatie', icon='bi-signpost', url_name='sign:index', order=2 ) # Create Organizations self.org_a = Organization.objects.create( name_fr="Commune A (FR)", name_nl="Commune A (NL)", code="ORG_A", organization_type="municipality" ) self.org_b = Organization.objects.create( name_fr="Commune B (FR)", name_nl="Commune B (NL)", code="ORG_B", organization_type="municipality" ) # Create users # 1. Superuser self.superuser = User.objects.create_superuser( username='super_org', password='testpass123' ) self.config_super = UserConfig.objects.create(user=self.superuser) self.config_super.accessible_views.add(self.sign_view) # 2. Restricted User for Org A (can manage RCP) self.user_a = User.objects.create_user( username='user_a', password='testpass123' ) self.config_a = UserConfig.objects.create( user=self.user_a, esign_can_manage_rcp=True ) self.config_a.accessible_views.add(self.sign_view) self.config_a.esign_organizations.add(self.org_a) # 3. Restricted User for Org B (can manage RCP) self.user_b = User.objects.create_user( username='user_b', password='testpass123' ) self.config_b = UserConfig.objects.create( user=self.user_b, esign_can_manage_rcp=True ) self.config_b.accessible_views.add(self.sign_view) self.config_b.esign_organizations.add(self.org_b) # 4. User without any allowed organizations (Visitor / read-only) self.user_visitor = User.objects.create_user( username='user_visitor', password='testpass123' ) self.config_visitor = UserConfig.objects.create( user=self.user_visitor ) self.config_visitor.accessible_views.add(self.sign_view) # Create some complementary police regulations (RCPs) self.reg_a = ComplementaryPoliceRegulation.objects.create( title="Regulation Org A", reference="REF-A-001", description="Test A", organization=self.org_a, status="in_preparation", uploaded_by=self.user_a ) self.reg_b = ComplementaryPoliceRegulation.objects.create( title="Regulation Org B", reference="REF-B-001", description="Test B", organization=self.org_b, status="in_preparation", uploaded_by=self.user_b ) def test_asset_forms_organization_required(self): """Test that SignPoleForm and SignPanelForm require an organization.""" import json from sign.forms import SignPoleForm, SignPanelForm # 1. SignPoleForm validation pole_data_no_org = { 'orientation': 90, 'lat': 50.85, 'lon': 4.35 } form_pole = SignPoleForm(data=pole_data_no_org) self.assertFalse(form_pole.is_valid()) self.assertIn('organization', form_pole.errors) pole_data_with_org = pole_data_no_org.copy() pole_data_with_org['organization'] = self.org_a.id form_pole_ok = SignPoleForm(data=pole_data_with_org) self.assertTrue(form_pole_ok.is_valid()) # 2. SignPanelForm validation panel_data_no_org = { 'orientation': 180, 'support_type': 'pole', 'is_temporary': False, 'lat': 50.85, 'lon': 4.35 } form_panel = SignPanelForm(data=panel_data_no_org) self.assertFalse(form_panel.is_valid()) self.assertIn('organization', form_panel.errors) panel_data_with_org = panel_data_no_org.copy() panel_data_with_org['organization'] = self.org_a.id form_panel_ok = SignPanelForm(data=panel_data_with_org) self.assertTrue(form_panel_ok.is_valid()) def test_universal_asset_visibility(self): """Test that any logged-in user can view assets without being blocked by organization permissions.""" import json self.client.force_login(self.user_visitor) response = self.client.get(reverse('sign:index')) self.assertEqual(response.status_code, 200) response_poles = self.client.post( reverse('sign:sign_poles_overview_geojson'), data=json.dumps({}), content_type='application/json' ) self.assertEqual(response_poles.status_code, 200) def test_superuser_bypass_restrictions(self): """Test that superusers can see and modify any regulation across all organizations.""" import json self.client.force_login(self.superuser) # 1. Superuser should see both regulations in list response = self.client.get(reverse('sign:regulations_list')) self.assertEqual(response.status_code, 200) self.assertContains(response, self.reg_a.title) self.assertContains(response, self.reg_b.title) # 2. Superuser should see both regulations in search response_search = self.client.get(reverse('sign:regulations_search'), {'q': ''}) self.assertEqual(response_search.status_code, 200) results = response_search.json().get('results', []) self.assertEqual(len(results), 2) # 3. Superuser can access detail of both response_detail_a = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_a.id})) self.assertEqual(response_detail_a.status_code, 200) response_detail_b = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_b.id})) self.assertEqual(response_detail_b.status_code, 200) def test_restricted_user_visibility_and_access(self): """Test that a restricted user can only see and access regulations of their organization.""" import json self.client.force_login(self.user_a) # 1. List only contains reg_a response_list = self.client.get(reverse('sign:regulations_list')) self.assertEqual(response_list.status_code, 200) self.assertContains(response_list, self.reg_a.title) self.assertNotContains(response_list, self.reg_b.title) # 2. Search only returns reg_a response_search = self.client.get(reverse('sign:regulations_search'), {'q': ''}) self.assertEqual(response_search.status_code, 200) results = response_search.json().get('results', []) self.assertEqual(len(results), 1) self.assertEqual(results[0]['id'], self.reg_a.id) # 3. Detail view for reg_a works, but reg_b raises PermissionDenied (403) response_detail_a = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_a.id})) self.assertEqual(response_detail_a.status_code, 200) response_detail_b = self.client.get(reverse('sign:regulation_detail', kwargs={'regulation_id': self.reg_b.id})) self.assertEqual(response_detail_b.status_code, 403) # 4. Docx generation for reg_a works (or handles it, but not 403), but reg_b raises 403 response_docx_b = self.client.get(reverse('sign:regulation_generate_docx', kwargs={'regulation_id': self.reg_b.id})) self.assertEqual(response_docx_b.status_code, 403) def test_regulation_creation_organization_check(self): """Test that creating a regulation is limited to the user's allowed organizations.""" self.client.force_login(self.user_a) # 1. Try creating a regulation for org_b (not allowed) response_create_forbidden = self.client.post( reverse('sign:regulation_create'), { 'title': 'New Reg Forbidden', 'reference': 'REF-F-001', 'description': 'Description', 'organization': self.org_b.id } ) self.assertEqual(response_create_forbidden.status_code, 403) # 2. Try creating a regulation for org_a (allowed) response_create_ok = self.client.post( reverse('sign:regulation_create'), { 'title': 'New Reg Allowed', 'reference': 'REF-OK-001', 'description': 'Description', 'organization': self.org_a.id } ) self.assertEqual(response_create_ok.status_code, 200) self.assertTrue(response_create_ok.json().get('success')) def test_regulation_editing_organization_check(self): """Test that editing a regulation and updating its organization checks user permissions.""" self.client.force_login(self.user_a) # 1. Edit reg_a: change title, keep org_a -> OK response_edit_ok = self.client.post( reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_a.id}), { 'title': 'Updated Title', 'reference': self.reg_a.reference, 'description': self.reg_a.description, 'organization': self.org_a.id } ) self.assertEqual(response_edit_ok.status_code, 200) self.assertTrue(response_edit_ok.json().get('success')) # 2. Edit reg_a: try to change organization to org_b -> Forbidden response_edit_forbidden = self.client.post( reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_a.id}), { 'title': 'Updated Title', 'reference': self.reg_a.reference, 'description': self.reg_a.description, 'organization': self.org_b.id } ) self.assertEqual(response_edit_forbidden.status_code, 403) # 3. Try to edit reg_b (which belongs to org_b) -> Forbidden (user_can_edit_regulation checks organization) response_edit_other_rcp = self.client.post( reverse('sign:regulation_edit', kwargs={'regulation_id': self.reg_b.id}), { 'title': 'Updated Title Other', 'reference': self.reg_b.reference, 'description': self.reg_b.description, 'organization': self.org_b.id } ) self.assertEqual(response_edit_other_rcp.status_code, 403)