loko/streetup/stock/tests_permissions.py
2026-07-22 14:48:40 +02:00

529 lines
20 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)
self.assertTrue(any("Le sous-type 'Correction' est réservé" 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()