530 lines
21 KiB
Python
530 lines
21 KiB
Python
"""
|
|
Tests unitaires pour le système de permissions des entrepôts (warehouses).
|
|
|
|
Pour exécuter ces tests :
|
|
python manage.py test stock.tests_permissions
|
|
"""
|
|
|
|
from django.test import TestCase, RequestFactory
|
|
from django.contrib.auth.models import User
|
|
from django.shortcuts import reverse
|
|
from django.http import HttpRequest
|
|
|
|
from common.models import UserConfig, UserWarehouseAccess
|
|
from stock.models import Warehouse, WarehouseLocation, Product, ProductCategory, Stock
|
|
from stock.permissions import (
|
|
user_can_view_warehouse,
|
|
user_can_manage_warehouse,
|
|
get_user_accessible_warehouses,
|
|
filter_warehouses_by_permission,
|
|
filter_locations_by_permission,
|
|
filter_products_by_permission,
|
|
)
|
|
|
|
|
|
class WarehousePermissionsTestCase(TestCase):
|
|
"""Tests pour les permissions des entrepôts."""
|
|
|
|
def setUp(self):
|
|
"""Configuration initiale des tests."""
|
|
# Créer des utilisateurs
|
|
self.superuser = User.objects.create_superuser(
|
|
username='admin',
|
|
email='admin@test.com',
|
|
password='admin123'
|
|
)
|
|
|
|
self.staff_user = User.objects.create_user(
|
|
username='staff',
|
|
email='staff@test.com',
|
|
password='staff123',
|
|
is_staff=True
|
|
)
|
|
|
|
self.regular_user = User.objects.create_user(
|
|
username='regular',
|
|
email='regular@test.com',
|
|
password='regular123'
|
|
)
|
|
|
|
self.restricted_user = User.objects.create_user(
|
|
username='restricted',
|
|
email='restricted@test.com',
|
|
password='restricted123'
|
|
)
|
|
|
|
# Créer les configurations utilisateur
|
|
self.regular_config = UserConfig.objects.create(user=self.regular_user)
|
|
self.restricted_config = UserConfig.objects.create(user=self.restricted_user)
|
|
|
|
# Créer des entrepôts
|
|
self.warehouse1 = Warehouse.objects.create(
|
|
code='WH-001',
|
|
name='Entrepôt Principal',
|
|
location='Bruxelles',
|
|
manager='Manager 1'
|
|
)
|
|
|
|
self.warehouse2 = Warehouse.objects.create(
|
|
code='WH-002',
|
|
name='Entrepôt Secondaire',
|
|
location='Anvers',
|
|
manager='Manager 2'
|
|
)
|
|
|
|
self.warehouse3 = Warehouse.objects.create(
|
|
code='WH-003',
|
|
name='Entrepôt Régional',
|
|
location='Liège',
|
|
manager='Manager 3'
|
|
)
|
|
|
|
# Créer des emplacements
|
|
self.location1 = WarehouseLocation.objects.create(
|
|
warehouse=self.warehouse1,
|
|
code='LOC-A1',
|
|
name='Zone A1'
|
|
)
|
|
|
|
self.location2 = WarehouseLocation.objects.create(
|
|
warehouse=self.warehouse2,
|
|
code='LOC-B1',
|
|
name='Zone B1'
|
|
)
|
|
|
|
# Créer une catégorie et des produits
|
|
self.category = ProductCategory.objects.create(
|
|
code='CAT-001',
|
|
name_fr='Matériel',
|
|
name_en='Material',
|
|
name_nl='Materiaal'
|
|
)
|
|
|
|
self.product1 = Product.objects.create(
|
|
code='PROD-001',
|
|
name='Produit 1',
|
|
category=self.category,
|
|
sku='SKU-001',
|
|
threshold=10
|
|
)
|
|
|
|
self.product2 = Product.objects.create(
|
|
code='PROD-002',
|
|
name='Produit 2',
|
|
category=self.category,
|
|
sku='SKU-002',
|
|
threshold=5
|
|
)
|
|
|
|
# Créer des stocks
|
|
Stock.objects.create(
|
|
product=self.product1,
|
|
location=self.location1,
|
|
quantity=100
|
|
)
|
|
|
|
Stock.objects.create(
|
|
product=self.product2,
|
|
location=self.location2,
|
|
quantity=50
|
|
)
|
|
|
|
# Configurer les permissions pour l'utilisateur restreint
|
|
# Accès en lecture seule au warehouse1
|
|
UserWarehouseAccess.objects.create(
|
|
user_config=self.restricted_config,
|
|
warehouse=self.warehouse1,
|
|
can_view=True,
|
|
can_manage=False
|
|
)
|
|
|
|
# Accès en gestion au warehouse2
|
|
UserWarehouseAccess.objects.create(
|
|
user_config=self.restricted_config,
|
|
warehouse=self.warehouse2,
|
|
can_view=True,
|
|
can_manage=True
|
|
)
|
|
|
|
def test_superuser_has_full_access(self):
|
|
"""Un superuser doit avoir accès à tous les entrepôts."""
|
|
self.assertTrue(user_can_view_warehouse(self.superuser, self.warehouse1))
|
|
self.assertTrue(user_can_view_warehouse(self.superuser, self.warehouse2))
|
|
self.assertTrue(user_can_view_warehouse(self.superuser, self.warehouse3))
|
|
|
|
self.assertTrue(user_can_manage_warehouse(self.superuser, self.warehouse1))
|
|
self.assertTrue(user_can_manage_warehouse(self.superuser, self.warehouse2))
|
|
self.assertTrue(user_can_manage_warehouse(self.superuser, self.warehouse3))
|
|
|
|
accessible = get_user_accessible_warehouses(self.superuser)
|
|
self.assertEqual(accessible.count(), 3)
|
|
|
|
def test_staff_has_full_access(self):
|
|
"""Un staff member doit avoir accès à tous les entrepôts."""
|
|
self.assertTrue(user_can_view_warehouse(self.staff_user, self.warehouse1))
|
|
self.assertTrue(user_can_view_warehouse(self.staff_user, self.warehouse2))
|
|
self.assertTrue(user_can_view_warehouse(self.staff_user, self.warehouse3))
|
|
|
|
self.assertTrue(user_can_manage_warehouse(self.staff_user, self.warehouse1))
|
|
self.assertTrue(user_can_manage_warehouse(self.staff_user, self.warehouse2))
|
|
self.assertTrue(user_can_manage_warehouse(self.staff_user, self.warehouse3))
|
|
|
|
def test_regular_user_has_default_access(self):
|
|
"""Un utilisateur normal sans restriction doit avoir accès à tout."""
|
|
self.assertTrue(user_can_view_warehouse(self.regular_user, self.warehouse1))
|
|
self.assertTrue(user_can_view_warehouse(self.regular_user, self.warehouse2))
|
|
self.assertTrue(user_can_view_warehouse(self.regular_user, self.warehouse3))
|
|
|
|
accessible = get_user_accessible_warehouses(self.regular_user)
|
|
self.assertEqual(accessible.count(), 3)
|
|
|
|
def test_restricted_user_view_permissions(self):
|
|
"""Un utilisateur restreint doit avoir accès uniquement à ses entrepôts autorisés."""
|
|
# Peut voir warehouse1 et warehouse2
|
|
self.assertTrue(user_can_view_warehouse(self.restricted_user, self.warehouse1))
|
|
self.assertTrue(user_can_view_warehouse(self.restricted_user, self.warehouse2))
|
|
|
|
# Ne peut pas voir warehouse3
|
|
self.assertFalse(user_can_view_warehouse(self.restricted_user, self.warehouse3))
|
|
|
|
# Vérifier la liste des entrepôts accessibles
|
|
accessible = get_user_accessible_warehouses(self.restricted_user)
|
|
self.assertEqual(accessible.count(), 2)
|
|
self.assertIn(self.warehouse1, accessible)
|
|
self.assertIn(self.warehouse2, accessible)
|
|
self.assertNotIn(self.warehouse3, accessible)
|
|
|
|
def test_restricted_user_manage_permissions(self):
|
|
"""Vérifier les permissions de gestion pour l'utilisateur restreint."""
|
|
# Ne peut pas gérer warehouse1 (seulement visualiser)
|
|
self.assertFalse(user_can_manage_warehouse(self.restricted_user, self.warehouse1))
|
|
|
|
# Peut gérer warehouse2
|
|
self.assertTrue(user_can_manage_warehouse(self.restricted_user, self.warehouse2))
|
|
|
|
# Ne peut pas gérer warehouse3
|
|
self.assertFalse(user_can_manage_warehouse(self.restricted_user, self.warehouse3))
|
|
|
|
# Vérifier la liste des entrepôts gérables
|
|
manageable = get_user_accessible_warehouses(self.restricted_user, can_manage=True)
|
|
self.assertEqual(manageable.count(), 1)
|
|
self.assertEqual(manageable.first(), self.warehouse2)
|
|
|
|
def test_filter_warehouses_queryset(self):
|
|
"""Tester le filtrage des querysets de warehouses."""
|
|
all_warehouses = Warehouse.objects.all()
|
|
|
|
# Pour le superuser : tous les entrepôts
|
|
filtered = filter_warehouses_by_permission(all_warehouses, self.superuser)
|
|
self.assertEqual(filtered.count(), 3)
|
|
|
|
# Pour l'utilisateur restreint : seulement 2 entrepôts
|
|
filtered = filter_warehouses_by_permission(all_warehouses, self.restricted_user)
|
|
self.assertEqual(filtered.count(), 2)
|
|
|
|
# Pour l'utilisateur restreint avec can_manage : seulement 1 entrepôt
|
|
filtered = filter_warehouses_by_permission(
|
|
all_warehouses,
|
|
self.restricted_user,
|
|
can_manage=True
|
|
)
|
|
self.assertEqual(filtered.count(), 1)
|
|
|
|
def test_filter_locations_queryset(self):
|
|
"""Tester le filtrage des emplacements selon les permissions."""
|
|
all_locations = WarehouseLocation.objects.all()
|
|
|
|
# Pour le superuser : tous les emplacements
|
|
filtered = filter_locations_by_permission(all_locations, self.superuser)
|
|
self.assertEqual(filtered.count(), 2)
|
|
|
|
# Pour l'utilisateur restreint : emplacements des entrepôts autorisés
|
|
filtered = filter_locations_by_permission(all_locations, self.restricted_user)
|
|
self.assertEqual(filtered.count(), 2)
|
|
self.assertIn(self.location1, filtered)
|
|
self.assertIn(self.location2, filtered)
|
|
|
|
def test_filter_products_queryset(self):
|
|
"""Tester le filtrage des produits selon les permissions."""
|
|
all_products = Product.objects.all()
|
|
|
|
# Pour le superuser : tous les produits
|
|
filtered = filter_products_by_permission(all_products, self.superuser)
|
|
self.assertEqual(filtered.count(), 2)
|
|
|
|
# Pour l'utilisateur restreint : produits dans les entrepôts autorisés
|
|
filtered = filter_products_by_permission(all_products, self.restricted_user)
|
|
self.assertEqual(filtered.count(), 2)
|
|
self.assertIn(self.product1, filtered)
|
|
self.assertIn(self.product2, filtered)
|
|
|
|
def test_anonymous_user_no_access(self):
|
|
"""Un utilisateur anonyme ne doit avoir aucun accès."""
|
|
anonymous = User() # Utilisateur non authentifié
|
|
|
|
self.assertFalse(user_can_view_warehouse(anonymous, self.warehouse1))
|
|
self.assertFalse(user_can_manage_warehouse(anonymous, self.warehouse1))
|
|
|
|
accessible = get_user_accessible_warehouses(anonymous)
|
|
self.assertEqual(accessible.count(), 0)
|
|
|
|
def test_warehouse_access_unique_constraint(self):
|
|
"""Vérifier qu'on ne peut pas créer de doublons d'accès."""
|
|
with self.assertRaises(Exception):
|
|
UserWarehouseAccess.objects.create(
|
|
user_config=self.restricted_config,
|
|
warehouse=self.warehouse1, # Déjà existant
|
|
can_view=True,
|
|
can_manage=False
|
|
)
|
|
|
|
def test_user_config_methods(self):
|
|
"""Tester les méthodes du modèle UserConfig."""
|
|
# Tester can_view_warehouse
|
|
self.assertTrue(self.restricted_config.can_view_warehouse(self.warehouse1))
|
|
self.assertFalse(self.restricted_config.can_view_warehouse(self.warehouse3))
|
|
|
|
# Tester can_manage_warehouse
|
|
self.assertTrue(self.restricted_config.can_manage_warehouse(self.warehouse2))
|
|
self.assertFalse(self.restricted_config.can_manage_warehouse(self.warehouse1))
|
|
|
|
# Tester get_accessible_warehouses
|
|
warehouses = self.restricted_config.get_accessible_warehouses()
|
|
self.assertEqual(warehouses.count(), 2)
|
|
|
|
manageable = self.restricted_config.get_accessible_warehouses(can_manage=True)
|
|
self.assertEqual(manageable.count(), 1)
|
|
|
|
|
|
class WarehouseViewsPermissionsTestCase(TestCase):
|
|
"""Tests d'intégration pour les vues avec permissions."""
|
|
|
|
def setUp(self):
|
|
"""Configuration des tests de vues."""
|
|
self.factory = RequestFactory()
|
|
|
|
# Créer un utilisateur avec restrictions
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
password='testpass123'
|
|
)
|
|
self.user_config = UserConfig.objects.create(user=self.user)
|
|
|
|
# Créer des entrepôts
|
|
self.accessible_warehouse = Warehouse.objects.create(
|
|
code='WH-A',
|
|
name='Entrepôt Accessible',
|
|
manager='Manager A'
|
|
)
|
|
|
|
self.forbidden_warehouse = Warehouse.objects.create(
|
|
code='WH-F',
|
|
name='Entrepôt Interdit',
|
|
manager='Manager F'
|
|
)
|
|
|
|
# Donner accès uniquement au premier entrepôt
|
|
UserWarehouseAccess.objects.create(
|
|
user_config=self.user_config,
|
|
warehouse=self.accessible_warehouse,
|
|
can_view=True,
|
|
can_manage=False
|
|
)
|
|
|
|
def test_warehouses_index_filters_correctly(self):
|
|
"""La page index doit filtrer les entrepôts selon les permissions."""
|
|
from stock.views import warehouses_index
|
|
|
|
request = self.factory.get('/stock/warehouses/')
|
|
request.user = self.user
|
|
|
|
response = warehouses_index(request)
|
|
|
|
# Vérifier que seuls les entrepôts autorisés sont dans le contexte
|
|
# Note: Cette partie nécessite d'accéder au contexte du template
|
|
# ce qui peut nécessiter des ajustements selon votre configuration
|
|
|
|
def test_warehouse_detail_redirects_on_no_permission(self):
|
|
"""Le détail d'un entrepôt interdit doit rediriger."""
|
|
from stock.views import warehouse_detail
|
|
|
|
request = self.factory.get(f'/stock/warehouse/{self.forbidden_warehouse.id}/')
|
|
request.user = self.user
|
|
|
|
# Ajouter le middleware de messages (nécessaire pour messages.error)
|
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
|
setattr(request, 'session', 'session')
|
|
messages = FallbackStorage(request)
|
|
setattr(request, '_messages', messages)
|
|
|
|
response = warehouse_detail(request, self.forbidden_warehouse.id)
|
|
|
|
# Vérifier la redirection
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_quick_stock_adjustment_permissions(self):
|
|
"""Tester les permissions de quick_stock_adjustment."""
|
|
from stock.views import quick_stock_adjustment
|
|
|
|
# Create a location for the accessible warehouse
|
|
loc = WarehouseLocation.objects.create(
|
|
warehouse=self.accessible_warehouse,
|
|
code='LOC-ACC',
|
|
name='Zone Accessible'
|
|
)
|
|
# Create a product
|
|
cat = ProductCategory.objects.create(code='CAT-T', name_fr='Test')
|
|
prod = Product.objects.create(code='PROD-T', name='Prod Test', category=cat, sku='SKU-T')
|
|
|
|
# Scenario 1: User only has can_manage=True on the warehouse (not can_administer)
|
|
access = UserWarehouseAccess.objects.get(user_config=self.user_config, warehouse=self.accessible_warehouse)
|
|
access.can_manage = True
|
|
access.can_administer = False
|
|
access.save()
|
|
|
|
request = self.factory.post('/stock/quick-stock-adjustment/', {
|
|
'product_id': prod.id,
|
|
'location_id': loc.id,
|
|
'quantity': 10,
|
|
'reason': 'Test Adjustment'
|
|
})
|
|
request.user = self.user
|
|
|
|
# Adjusting should fail (403 Permission Denied) since they are not an administrator
|
|
response = quick_stock_adjustment(request)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
# Scenario 2: User has can_administer=True on the warehouse
|
|
access.can_administer = True
|
|
access.save()
|
|
|
|
response = quick_stock_adjustment(request)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_batch_stock_correction_subtype_permissions(self):
|
|
"""Tester que le sous-type 'correction' requiert les droits d'administration de l'entrepôt."""
|
|
from stock.views import new_stock_entry
|
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
|
from common.models import Role
|
|
|
|
# Grant stock_manager role to allow access to _new_stock_batch_view
|
|
stock_manager_role, _ = Role.objects.get_or_create(name='stock_manager')
|
|
self.user_config.roles.add(stock_manager_role)
|
|
|
|
# Create a location for the accessible warehouse
|
|
loc = WarehouseLocation.objects.create(
|
|
warehouse=self.accessible_warehouse,
|
|
code='LOC-ACC2',
|
|
name='Zone Accessible 2'
|
|
)
|
|
# Create a product
|
|
cat = ProductCategory.objects.create(code='CAT-T2', name_fr='Test 2')
|
|
prod = Product.objects.create(code='PROD-T2', name='Prod Test 2', category=cat, sku='SKU-T2')
|
|
|
|
# Setup session and messages on the request
|
|
request = self.factory.post('/stock/new-entry/', {
|
|
'global_destination_location_id': loc.id,
|
|
'movement_subtype': 'correction',
|
|
'product_id': [prod.id],
|
|
'quantity': [5],
|
|
})
|
|
request.user = self.user
|
|
setattr(request, 'session', {})
|
|
messages = FallbackStorage(request)
|
|
setattr(request, '_messages', messages)
|
|
|
|
# Scenario 1: User has can_manage=True but can_administer=False
|
|
access = UserWarehouseAccess.objects.get(user_config=self.user_config, warehouse=self.accessible_warehouse)
|
|
access.can_manage = True
|
|
access.can_administer = False
|
|
access.save()
|
|
|
|
# Let's call the view
|
|
response = new_stock_entry(request)
|
|
# It should show error message and stay on the form page (not redirect to success)
|
|
from django.utils.translation import gettext as _
|
|
self.assertTrue(any(_("Le sous-type 'Correction' est réservé aux administrateurs de l'entrepôt.") in str(m) for m in messages))
|
|
|
|
# Scenario 2: User has can_administer=True
|
|
access.can_administer = True
|
|
access.save()
|
|
|
|
# Re-initialize request and messages
|
|
request = self.factory.post('/stock/new-entry/', {
|
|
'global_destination_location_id': loc.id,
|
|
'movement_subtype': 'correction',
|
|
'product_id': [prod.id],
|
|
'quantity': [5],
|
|
})
|
|
request.user = self.user
|
|
setattr(request, 'session', {})
|
|
messages = FallbackStorage(request)
|
|
setattr(request, '_messages', messages)
|
|
|
|
response = new_stock_entry(request)
|
|
# It should succeed and redirect (status 302) to the batch detail page
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_quick_stock_adjustment_negative_quantity(self):
|
|
"""Tester qu'un ajustement de stock rapide accepte une quantité négative."""
|
|
from stock.views import quick_stock_adjustment
|
|
|
|
loc = WarehouseLocation.objects.create(
|
|
warehouse=self.accessible_warehouse,
|
|
code='LOC-NEG',
|
|
name='Zone Négative'
|
|
)
|
|
cat = ProductCategory.objects.create(code='CAT-NEG', name_fr='Test Neg')
|
|
prod = Product.objects.create(code='PROD-NEG', name='Prod Neg', category=cat, sku='SKU-NEG')
|
|
|
|
# Create initial positive stock
|
|
stock = Stock.objects.create(product=prod, location=loc, quantity=5)
|
|
|
|
access = UserWarehouseAccess.objects.get(user_config=self.user_config, warehouse=self.accessible_warehouse)
|
|
access.can_administer = True
|
|
access.save()
|
|
|
|
# Post a negative quantity (-3)
|
|
request = self.factory.post('/stock/quick-stock-adjustment/', {
|
|
'product_id': prod.id,
|
|
'location_id': loc.id,
|
|
'quantity': -3,
|
|
'reason': 'Ajustement négatif'
|
|
})
|
|
request.user = self.user
|
|
|
|
response = quick_stock_adjustment(request)
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
stock.refresh_from_db()
|
|
self.assertEqual(stock.quantity, -3)
|
|
|
|
# Check stock movement is created correctly with quantity=abs(diff) = abs(-3 - 5) = 8
|
|
from stock.models import StockMovement
|
|
movement = StockMovement.objects.filter(product=prod).first()
|
|
self.assertIsNotNone(movement)
|
|
self.assertEqual(movement.quantity, 8)
|
|
self.assertEqual(movement.source_location, loc)
|
|
self.assertIsNone(movement.destination_location)
|
|
|
|
|
|
|
|
|
|
|
|
def run_tests():
|
|
"""Fonction helper pour exécuter les tests manuellement."""
|
|
import django
|
|
django.setup()
|
|
|
|
from django.test.utils import get_runner
|
|
from django.conf import settings
|
|
|
|
TestRunner = get_runner(settings)
|
|
test_runner = TestRunner()
|
|
failures = test_runner.run_tests(["stock.tests_permissions"])
|
|
|
|
return failures
|
|
|
|
|
|
if __name__ == '__main__':
|
|
run_tests()
|