loko/streetup/interventions/tests/test_visibility_permissions.py
2026-07-22 14:48:40 +02:00

473 lines
19 KiB
Python

import json
from datetime import timedelta
from unittest import mock
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.contrib import messages
from django.contrib.messages import get_messages
from django.core.files.uploadedfile import SimpleUploadedFile
from common.models import UserConfig, Role, Thematic, UserThematics
from contracts.models import Company, CompanyTeam, CompanyMember, Contract, ContractPost, ContractOrder, ContractOrderQuote, ContractOrderQuoteItem
from assets.models import AssetCategory
from interventions.models import (
Intervention,
InterventionContractPost,
InterventionPlanificationTimeLine,
InterventionRealizationTimeLine,
Operation,
InterventionAsset,
OperationTemplate,
OperationTemplateItem,
OperationStatusSet,
OperationStatus,
STATUS_ORDERS,
InterventionDocument,
)
from django.contrib.contenttypes.models import ContentType
from interventions.views import _group_operations_by_asset_and_template, _create_equipment_operations_for_assets
from interventions.permissions import can_manage_occupations, can_delete_document
from django.urls import reverse
from django.utils import timezone
from decimal import Decimal
class OccupationPermissionsTests(TestCase):
def setUp(self):
self.User = get_user_model()
self.technician_role, _ = Role.objects.get_or_create(name='technician')
self.external_role, _ = Role.objects.get_or_create(name='external_manager')
self.company = Company.objects.create(name='Test Company')
self.team = CompanyTeam.objects.create(name='Team A', company=self.company)
def _create_user_with_role(self, username, role):
user = self.User.objects.create_user(username=username, password='pwd')
user_config = UserConfig.objects.create(user=user)
user_config.roles.add(role)
return user, user_config
def test_can_manage_occupations_for_assigned_technician(self):
user, _ = self._create_user_with_role('tech', self.technician_role)
member = CompanyMember.objects.create(user=user, name='Tech One')
member.teams.add(self.team)
intervention = Intervention.objects.create(title='Test intervention', assigned_member=member)
self.assertTrue(can_manage_occupations(user, intervention))
def test_cannot_manage_occupations_for_unrelated_technician(self):
user, _ = self._create_user_with_role('other-tech', self.technician_role)
member = CompanyMember.objects.create(user=user, name='Tech Two')
member.teams.add(self.team)
other_company = Company.objects.create(name='Other Company')
other_team = CompanyTeam.objects.create(name='Team B', company=other_company)
intervention = Intervention.objects.create(title='Another intervention', assigned_team=other_team)
self.assertFalse(can_manage_occupations(user, intervention))
def test_external_manager_can_manage(self):
user, _ = self._create_user_with_role('external', self.external_role)
intervention = Intervention.objects.create(title='External intervention')
self.assertTrue(can_manage_occupations(user, intervention))
class TeamDailyVisibilityTests(TestCase):
def setUp(self):
from contracts.models import Company, CompanyTeam, CompanyMember
from common.models import Role, UserConfig, Thematic, UserThematics, UserContractAccess
self.User = get_user_model()
self.tech_user = self.User.objects.create_user(username='tech-user', password='pwd')
self.role_tech, _ = Role.objects.get_or_create(name='technician')
self.tech_config = UserConfig.objects.create(user=self.tech_user, is_intern=False)
self.tech_config.roles.add(self.role_tech)
self.company = Company.objects.create(name='Test Company')
# Team with restriction: only today's interventions from 6 AM
from datetime import time
self.team = CompanyTeam.objects.create(
name='Restricted Team',
company=self.company,
restrict_today_interventions_visibility=True,
visibility_start_hour=time(6, 0)
)
self.member = CompanyMember.objects.create(user=self.tech_user, name='Technician Doe')
self.member.teams.add(self.team)
self.thematic = Thematic.objects.create(code='roads', name_fr='Voirie', name_nl='Wegen')
UserThematics.objects.create(
user_config=self.tech_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
# Grant access via contract as well
self.contract = Contract.objects.create(
company=self.company,
contract_number='C-TEST',
start_date=timezone.now().date() - timedelta(days=10),
end_date=timezone.now().date() + timedelta(days=10),
is_active=True
)
UserContractAccess.objects.create(
user_config=self.tech_config,
contract=self.contract,
can_view_interventions=True,
)
@mock.patch('django.utils.timezone.now')
def test_visibility_before_and_after_start_hour(self, mock_now):
from datetime import datetime
import pytz
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Fix today to 2026-06-17
# 1. Test before 6 AM (3:30 AM UTC = 5:30 AM Brussels local time CEST)
mock_now.return_value = datetime(2026, 6, 17, 3, 30, 0, tzinfo=pytz.UTC)
# Create today's, yesterday's, and tomorrow's interventions
itv_today = Intervention.objects.create(
title='Today Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
planned_begin_time=datetime(2026, 6, 17, 10, 0, 0, tzinfo=pytz.UTC),
)
itv_yesterday = Intervention.objects.create(
title='Yesterday Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
planned_begin_time=datetime(2026, 6, 16, 10, 0, 0, tzinfo=pytz.UTC),
)
itv_tomorrow = Intervention.objects.create(
title='Tomorrow Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
planned_begin_time=datetime(2026, 6, 18, 10, 0, 0, tzinfo=pytz.UTC),
)
# Before 6 AM, technician should not see anything (empty queryset and False for can_view)
self.assertFalse(can_view_intervention(self.tech_user, itv_today))
self.assertFalse(can_view_intervention(self.tech_user, itv_yesterday))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertEqual(visible_qs.count(), 0)
# 2. Test after 6 AM (7:30 AM UTC = 9:30 AM Brussels local time)
mock_now.return_value = datetime(2026, 6, 17, 7, 30, 0, tzinfo=pytz.UTC)
# After 6 AM, they should only be able to view today's intervention
self.assertTrue(can_view_intervention(self.tech_user, itv_today))
self.assertFalse(can_view_intervention(self.tech_user, itv_yesterday))
self.assertFalse(can_view_intervention(self.tech_user, itv_tomorrow))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertEqual(visible_qs.count(), 1)
self.assertIn(itv_today, visible_qs)
self.assertNotIn(itv_yesterday, visible_qs)
self.assertNotIn(itv_tomorrow, visible_qs)
@mock.patch('django.utils.timezone.now')
def test_visibility_privileged_users_not_restricted(self, mock_now):
from datetime import datetime
import pytz
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
from common.models import Role
# Fix today to 2026-06-17 at 3:30 AM UTC (before start hour)
mock_now.return_value = datetime(2026, 6, 17, 3, 30, 0, tzinfo=pytz.UTC)
itv_yesterday = Intervention.objects.create(
title='Yesterday Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
planned_begin_time=datetime(2026, 6, 16, 10, 0, 0, tzinfo=pytz.UTC),
)
# If user gets role external_manager, they should see it despite being in the restricted team
role_ext_mgr, _ = Role.objects.get_or_create(name='external_manager')
self.tech_config.roles.add(role_ext_mgr)
self.assertTrue(can_view_intervention(self.tech_user, itv_yesterday))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_yesterday, visible_qs)
class TeamAssignedOnlyVisibilityTests(TestCase):
def setUp(self):
from contracts.models import Company, CompanyTeam, CompanyMember
from common.models import Role, UserConfig, Thematic, UserThematics, UserContractAccess
self.User = get_user_model()
self.tech_user = self.User.objects.create_user(username='tech-user-2', password='pwd')
self.role_tech, _ = Role.objects.get_or_create(name='technician')
self.tech_config = UserConfig.objects.create(user=self.tech_user, is_intern=False)
self.tech_config.roles.add(self.role_tech)
self.company = Company.objects.create(name='Test Company 2')
# Restricted and Unrestricted Teams
self.team_restricted = CompanyTeam.objects.create(
name='Restricted Team 2',
company=self.company,
restrict_to_assigned_interventions=True
)
self.team_unrestricted = CompanyTeam.objects.create(
name='Unrestricted Team 2',
company=self.company,
restrict_to_assigned_interventions=False
)
self.other_team = CompanyTeam.objects.create(
name='Other Team 2',
company=self.company,
restrict_to_assigned_interventions=False
)
self.member = CompanyMember.objects.create(user=self.tech_user, name='Technician Doe 2')
self.thematic = Thematic.objects.create(code='roads2', name_fr='Voirie 2', name_nl='Wegen 2')
UserThematics.objects.create(
user_config=self.tech_config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
self.contract = Contract.objects.create(
company=self.company,
contract_number='C-TEST-2',
start_date=timezone.now().date() - timedelta(days=10),
end_date=timezone.now().date() + timedelta(days=10),
is_active=True
)
UserContractAccess.objects.create(
user_config=self.tech_config,
contract=self.contract,
can_view_interventions=True,
)
def test_visibility_restricted_team_only(self):
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Member only belongs to restricted team
self.member.teams.add(self.team_restricted)
# 1. Assigned to team
itv_team = Intervention.objects.create(
title='Team Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.team_restricted,
)
# 2. Assigned to member
itv_member = Intervention.objects.create(
title='Member Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_member=self.member,
)
# 3. Unassigned company task
itv_unassigned = Intervention.objects.create(
title='Unassigned Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
)
# 4. Assigned to other team
itv_other = Intervention.objects.create(
title='Other Team Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.other_team,
)
# Check individual visibility
self.assertTrue(can_view_intervention(self.tech_user, itv_team))
self.assertTrue(can_view_intervention(self.tech_user, itv_member))
self.assertFalse(can_view_intervention(self.tech_user, itv_unassigned))
self.assertFalse(can_view_intervention(self.tech_user, itv_other))
# Check list visibility
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertEqual(visible_qs.count(), 2)
self.assertIn(itv_team, visible_qs)
self.assertIn(itv_member, visible_qs)
self.assertNotIn(itv_unassigned, visible_qs)
self.assertNotIn(itv_other, visible_qs)
def test_visibility_unrestricted_team(self):
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Member only belongs to unrestricted team
self.member.teams.add(self.team_unrestricted)
# Unassigned company task
itv_unassigned = Intervention.objects.create(
title='Unassigned Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
)
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_unassigned, visible_qs)
def test_visibility_mixed_teams(self):
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Member belongs to both restricted and unrestricted teams of the same company
self.member.teams.add(self.team_restricted, self.team_unrestricted)
# Unassigned company task
itv_unassigned = Intervention.objects.create(
title='Unassigned Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
)
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_unassigned, visible_qs)
def test_visibility_privileged_user_not_restricted(self):
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
from common.models import Role
# Member belongs only to restricted team
self.member.teams.add(self.team_restricted)
# Give the user a privileged role: operator
role_operator, _ = Role.objects.get_or_create(name='operator')
self.tech_config.roles.add(role_operator)
# Unassigned company task
itv_unassigned = Intervention.objects.create(
title='Unassigned Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
)
self.assertTrue(can_view_intervention(self.tech_user, itv_unassigned))
visible_qs = filter_viewable_interventions_for_user(self.tech_user)
self.assertIn(itv_unassigned, visible_qs)
def test_is_restricted_team_context(self):
from interventions.models import Intervention
from django.urls import reverse
# 1. User belongs only to restricted team
self.member.teams.add(self.team_restricted)
itv = Intervention.objects.create(
title='Team Task',
status='to_be_processed',
contract=self.contract,
thematic=self.thematic,
assigned_provider=self.company,
assigned_team=self.team_restricted,
)
self.client.login(username='tech-user-2', password='pwd')
url = reverse('mobile:intervention_detail_mobile', args=[itv.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTrue(resp.context['is_restricted_team'])
# 2. User has an unrestricted team too
self.member.teams.add(self.team_unrestricted)
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context['is_restricted_team'])
class CreatorVisibilityTests(TestCase):
def setUp(self):
from django.contrib.auth import get_user_model
from common.models import Role, UserConfig, Thematic, UserThematics
self.User = get_user_model()
self.user1 = self.User.objects.create_user(username='creator-user-1', password='pwd')
self.user2 = self.User.objects.create_user(username='other-user-2', password='pwd')
self.role_tech, _ = Role.objects.get_or_create(name='technician')
self.config1 = UserConfig.objects.create(user=self.user1, is_intern=False, limit_interventions_to_contracts=True)
self.config1.roles.add(self.role_tech)
self.config2 = UserConfig.objects.create(user=self.user2, is_intern=False, limit_interventions_to_contracts=True)
self.config2.roles.add(self.role_tech)
self.thematic = Thematic.objects.create(code='roads_creator', name_fr='Voirie Creator', name_nl='Wegen Creator')
UserThematics.objects.create(
user_config=self.config1,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
UserThematics.objects.create(
user_config=self.config2,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
)
def test_creator_can_view_own_created_intervention(self):
from interventions.models import Intervention
from interventions.permissions import can_view_intervention, filter_viewable_interventions_for_user
# Create intervention with no contract, created by user1
itv = Intervention.objects.create(
title='Created by User 1',
status='to_be_processed',
contract=None,
thematic=self.thematic,
created_by=self.user1,
)
# User 1 should see it (creator)
self.assertTrue(can_view_intervention(self.user1, itv))
self.assertIn(itv, filter_viewable_interventions_for_user(self.user1))
# User 2 should NOT see it (not the creator, and no contract assigned)
self.assertFalse(can_view_intervention(self.user2, itv))
self.assertNotIn(itv, filter_viewable_interventions_for_user(self.user2))