This commit is contained in:
18
backend/modules/fleet/tests/conftest.py
Normal file
18
backend/modules/fleet/tests/conftest.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# conftest.py
|
||||
import pytest
|
||||
import uuid
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
@pytest.fixture
|
||||
def test_user():
|
||||
return get_user_model().objects.create_user(
|
||||
username=f"user_{uuid.uuid4().hex[:8]}",
|
||||
email=f"user+{uuid.uuid4().hex[:8]}@example.test",
|
||||
password="pass"
|
||||
)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_test_celery_settings(settings):
|
||||
settings.CELERY_TASK_ALWAYS_EAGER = True
|
||||
settings.CELERY_TASK_EAGER_PROPAGATES=True # Ovo osigurava da greške u tasku odmah pucaju u testu
|
||||
settings.BROKER_BACKEND='memory', # Koristi in-memory broker umjesto Redis-a
|
||||
72
backend/modules/fleet/tests/test_bulk_close_work_orders.py
Normal file
72
backend/modules/fleet/tests/test_bulk_close_work_orders.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
from modules.fleet.models import Vehicle, WorkOrder
|
||||
from modules.fleet.services import FleetService
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bulk_close_work_orders_updates_mileage(test_user):
|
||||
|
||||
# 1. Kreiraj vozilo
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-001", current_mileage=1000)
|
||||
|
||||
# 2. Kreiraj više otvorenih naloga
|
||||
wo1 = WorkOrder.objects.create(vehicle=vehicle, creator=test_user, start_mileage=1000, end_mileage=1200)
|
||||
wo2 = WorkOrder.objects.create(vehicle=vehicle, creator=test_user, start_mileage=1200, end_mileage=1500)
|
||||
|
||||
# 3. Pozovi bulk servis
|
||||
queryset = WorkOrder.objects.filter(pk__in=[wo1.pk, wo2.pk])
|
||||
FleetService.bulk_close_work_orders(queryset)
|
||||
|
||||
# 4. Provjeri rezultate
|
||||
vehicle.refresh_from_db()
|
||||
wo1.refresh_from_db()
|
||||
wo2.refresh_from_db()
|
||||
|
||||
# Statusi moraju biti zatvoreni
|
||||
assert wo1.status == 'closed'
|
||||
assert wo2.status == 'closed'
|
||||
|
||||
# Kilometraža vozila mora biti najveća (1500)
|
||||
assert vehicle.current_mileage == 1500
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bulk_close_ignores_already_closed_orders(test_user):
|
||||
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-002", current_mileage=1000)
|
||||
|
||||
# Već zatvoren nalog
|
||||
wo = WorkOrder.objects.create(vehicle=vehicle, creator=test_user, start_mileage=1000, end_mileage=1100, status='closed')
|
||||
|
||||
# Pozovi servis
|
||||
FleetService.bulk_close_work_orders(WorkOrder.objects.filter(pk=wo.pk))
|
||||
|
||||
# Provjeri da nalog nije "pukao" niti promijenio stanje
|
||||
wo.refresh_from_db()
|
||||
assert wo.status == 'closed'
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_bulk_close_ignores_incomplete_work_order(test_user):
|
||||
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-003", current_mileage=1000)
|
||||
|
||||
# Nalog koji je otvoren i NEMA end_mileage
|
||||
wo = WorkOrder.objects.create(
|
||||
vehicle=vehicle,
|
||||
creator=test_user,
|
||||
start_mileage=1000,
|
||||
end_mileage=None, # Incomplete
|
||||
status='open'
|
||||
)
|
||||
|
||||
# Pozovi servis
|
||||
FleetService.bulk_close_work_orders(WorkOrder.objects.filter(pk=wo.pk))
|
||||
|
||||
# Provjeri: Nalog bi trebao ostati otvoren ili se možda ne bi smio zatvoriti
|
||||
# jer podaci nisu kompletni
|
||||
wo.refresh_from_db()
|
||||
vehicle.refresh_from_db()
|
||||
|
||||
# Ovisno o tvojoj poslovnoj logici:
|
||||
# Ako nalog nije kompletan, ne bi ga se smjelo zatvoriti.
|
||||
assert wo.status == 'open'
|
||||
assert vehicle.current_mileage == 1000
|
||||
67
backend/modules/fleet/tests/test_crane_serial_number.py
Normal file
67
backend/modules/fleet/tests/test_crane_serial_number.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import uuid
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from modules.fleet.serializers import CraneSerializer
|
||||
|
||||
|
||||
class CraneSerialNumberTests(TestCase):
|
||||
def _base_payload(self):
|
||||
suffix = uuid.uuid4().hex[:6].upper()
|
||||
return {
|
||||
"registration_number": f"ZG-CRN-{suffix}",
|
||||
"make": "Liebherr",
|
||||
"model": "LTM",
|
||||
"year": 2024,
|
||||
"current_mileage": 12000,
|
||||
"service_interval_km": 8000,
|
||||
"is_company_vehicle": True,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
def test_create_crane_with_serial_number_succeeds(self):
|
||||
payload = {
|
||||
**self._base_payload(),
|
||||
"crane_serial_number": "123456",
|
||||
}
|
||||
serializer = CraneSerializer(data=payload)
|
||||
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||
|
||||
crane = serializer.save()
|
||||
self.assertEqual(crane.asset_type, "crane")
|
||||
self.assertEqual(crane.crane_serial_number, "123456")
|
||||
|
||||
def test_create_crane_without_serial_number_fails(self):
|
||||
payload = self._base_payload()
|
||||
serializer = CraneSerializer(data=payload)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("crane_serial_number", serializer.errors)
|
||||
|
||||
def test_crane_serial_number_industry_standard_validation(self):
|
||||
valid_payload = {
|
||||
**self._base_payload(),
|
||||
"registration_number": f"ZG-CRN-{uuid.uuid4().hex[:6].upper()}",
|
||||
"crane_serial_number": "000123",
|
||||
}
|
||||
valid_serializer = CraneSerializer(data=valid_payload)
|
||||
self.assertTrue(valid_serializer.is_valid(), valid_serializer.errors)
|
||||
|
||||
invalid_serials = [
|
||||
"12345", # prekratko
|
||||
"1234567", # predugo
|
||||
"12A456", # slova nisu dozvoljena
|
||||
"12 456", # razmaci nisu dozvoljeni
|
||||
"12-456", # specijalni znakovi nisu dozvoljeni
|
||||
"123456", # unicode znamenke nisu dozvoljene (samo ASCII 0-9)
|
||||
]
|
||||
|
||||
for value in invalid_serials:
|
||||
with self.subTest(serial=value):
|
||||
payload = {
|
||||
**self._base_payload(),
|
||||
"registration_number": f"ZG-CRN-{uuid.uuid4().hex[:6].upper()}",
|
||||
"crane_serial_number": value,
|
||||
}
|
||||
serializer = CraneSerializer(data=payload)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("crane_serial_number", serializer.errors)
|
||||
24
backend/modules/fleet/tests/test_delete_work_order.py
Normal file
24
backend/modules/fleet/tests/test_delete_work_order.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleNotification
|
||||
from modules.fleet.services import VehicleService
|
||||
import uuid
|
||||
|
||||
class WorkOrderDeleteTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="u1", email=f"u1+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.other = User.objects.create_user(username="u2", email=f"u2+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.vehicle = Vehicle.objects.create(registration_number=f"V-{uuid.uuid4().hex[:6]}", current_mileage=1000)
|
||||
self.wo = WorkOrder.objects.create(vehicle=self.vehicle, creator=self.user, start_mileage=1000)
|
||||
|
||||
def test_delete_by_creator_creates_notification(self):
|
||||
VehicleService.delete_work_order(self.wo, user=self.user)
|
||||
self.wo.refresh_from_db()
|
||||
self.assertFalse(self.wo.is_active)
|
||||
notif = VehicleNotification.objects.filter(recipient=self.user, title__icontains="obrisan").exists()
|
||||
self.assertTrue(notif)
|
||||
|
||||
def test_delete_by_non_creator_denied(self):
|
||||
with self.assertRaises(Exception):
|
||||
VehicleService.delete_work_order(self.wo, user=self.other)
|
||||
28
backend/modules/fleet/tests/test_integration.py
Normal file
28
backend/modules/fleet/tests/test_integration.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from modules.fleet.models import Vehicle
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_vehicle_servicer_one_to_one_constraint(test_user):
|
||||
|
||||
# Prvo vozilo prolazi
|
||||
Vehicle.objects.create(registration_number="ZG-111-AA", assigned_servicer=test_user)
|
||||
|
||||
# Drugo vozilo za istog servisera mora baciti IntegrityError
|
||||
with pytest.raises(Exception): # Django IntegrityError
|
||||
Vehicle.objects.create(registration_number="ZG-222-BB", assigned_servicer=test_user)
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_vehicle_next_service_logic():
|
||||
# current=12000, interval=15000 -> next_service_at = 15000
|
||||
vehicle = Vehicle.objects.create(
|
||||
registration_number="ZG-001",
|
||||
current_mileage=12000,
|
||||
service_interval_km=15000
|
||||
)
|
||||
assert vehicle.next_service_at() == 15000
|
||||
|
||||
# current=16000, interval=15000 -> next_service_at = 30000
|
||||
vehicle.current_mileage = 16000
|
||||
assert vehicle.next_service_at() == 30000
|
||||
33
backend/modules/fleet/tests/test_models.py
Normal file
33
backend/modules/fleet/tests/test_models.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
||||
|
||||
|
||||
class FleetModelTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="tech", email=f"tech+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
registration_number=f"ZG-{uuid.uuid4().hex[:6].upper()}",
|
||||
make="Toyota",
|
||||
model="Yaris",
|
||||
current_mileage=12000,
|
||||
service_interval_km=15000,
|
||||
assigned_servicer=self.user
|
||||
)
|
||||
|
||||
def test_next_service_at(self):
|
||||
self.assertEqual(self.vehicle.next_service_at(), 15000)
|
||||
|
||||
def test_work_order_updates_vehicle_mileage(self):
|
||||
wo = WorkOrder.objects.create(vehicle=self.vehicle, creator=self.user, start_mileage=12000, end_mileage=12500)
|
||||
self.vehicle.refresh_from_db()
|
||||
self.assertEqual(self.vehicle.current_mileage, 12500)
|
||||
self.assertEqual(wo.distance, 500)
|
||||
|
||||
def test_service_record_sets_next_service_due_and_updates_vehicle(self):
|
||||
rec = VehicleServiceRecord.objects.create(vehicle=self.vehicle, performed_by=self.user, description="Oil change", mileage=13000, parts="oil;filter", cost="50.00")
|
||||
self.vehicle.refresh_from_db()
|
||||
self.assertEqual(rec.next_service_due_at, 13000 + self.vehicle.service_interval_km)
|
||||
self.assertEqual(self.vehicle.current_mileage, 13000)
|
||||
201
backend/modules/fleet/tests/test_notifications.py
Normal file
201
backend/modules/fleet/tests/test_notifications.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import uuid
|
||||
from django.test import TestCase, override_settings
|
||||
from django.core import mail
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from django.contrib.auth import get_user_model
|
||||
from unittest.mock import patch
|
||||
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord, VehicleNotification
|
||||
from modules.fleet.services import VehicleService, NotificationService
|
||||
|
||||
|
||||
class NotificationEmailTests(TestCase):
|
||||
@override_settings(
|
||||
EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend',
|
||||
DEFAULT_FROM_EMAIL='no-reply@example.test'
|
||||
)
|
||||
def test_create_notification_sends_email_and_records_is_sent(self):
|
||||
User = get_user_model()
|
||||
user = User.objects.create_user(username=f"user_{uuid.uuid4().hex[:6]}", email=f"user+{uuid.uuid4()}@example.test", password="pass")
|
||||
|
||||
# Ensure outbox is empty
|
||||
mail.outbox.clear()
|
||||
|
||||
title = "Test email notification"
|
||||
message = "This is a test message."
|
||||
|
||||
with patch('django.db.transaction.on_commit', lambda f: f()):
|
||||
notif = NotificationService.create_notification(
|
||||
recipient=user,
|
||||
title=title,
|
||||
message=message,
|
||||
level="info",
|
||||
send_email=True
|
||||
)
|
||||
|
||||
# One email should have been sent
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
sent = mail.outbox[0]
|
||||
self.assertIn(title, sent.subject)
|
||||
self.assertIn(message, sent.body)
|
||||
# The notification record should exist and flagged as sent
|
||||
notif.refresh_from_db()
|
||||
self.assertTrue(notif.is_sent)
|
||||
self.assertEqual(notif.recipient, user)
|
||||
self.assertEqual(notif.title, title)
|
||||
|
||||
|
||||
class NotificationRealtimeTests(TestCase):
|
||||
@patch('modules.fleet.services.PusherService.broadcast_notification')
|
||||
def test_create_notification_broadcasts_after_on_commit(self, mock_broadcast):
|
||||
User = get_user_model()
|
||||
user = User.objects.create_user(
|
||||
username=f"ws_user_{uuid.uuid4().hex[:6]}",
|
||||
email=f"ws+{uuid.uuid4()}@example.test",
|
||||
password="pass"
|
||||
)
|
||||
|
||||
with patch('django.db.transaction.on_commit', lambda fn: fn()):
|
||||
notif = NotificationService.create_notification(
|
||||
recipient=user,
|
||||
title="Realtime test",
|
||||
message="Provjera pusher poruke",
|
||||
level="warning",
|
||||
send_email=False,
|
||||
metadata={"source": "unit-test"}
|
||||
)
|
||||
|
||||
self.assertEqual(notif.recipient, user)
|
||||
mock_broadcast.assert_called_once_with(
|
||||
user_id=user.id,
|
||||
title="Realtime test",
|
||||
message="Provjera pusher poruke",
|
||||
level="warning",
|
||||
notification_id=notif.id,
|
||||
metadata={"source": "unit-test"}
|
||||
)
|
||||
|
||||
@patch('modules.fleet.services.PusherService.broadcast_notification')
|
||||
def test_create_notification_without_recipient_does_not_broadcast(self, mock_broadcast):
|
||||
NotificationService.create_notification(
|
||||
recipient=None,
|
||||
title="No recipient",
|
||||
message="Broadcast se ne smije pozvati",
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
|
||||
mock_broadcast.assert_not_called()
|
||||
|
||||
|
||||
class WorkOrderAndServiceRecordNotificationTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
# creator who will create work order
|
||||
self.creator = User.objects.create_user(username=f"creator_{uuid.uuid4().hex[:6]}", email=f"creator+{uuid.uuid4()}@example.test", password="pass")
|
||||
# assigned servicer (different user)
|
||||
self.servicer = User.objects.create_user(username=f"servicer_{uuid.uuid4().hex[:6]}", email=f"servicer+{uuid.uuid4()}@example.test", password="pass")
|
||||
# another technician who will perform the service
|
||||
self.technician = User.objects.create_user(username=f"tech_{uuid.uuid4().hex[:6]}", email=f"tech+{uuid.uuid4()}@example.test", password="pass")
|
||||
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
registration_number=f"REG-{uuid.uuid4().hex[:6].upper()}",
|
||||
make="TestMake",
|
||||
model="TestModel",
|
||||
current_mileage=10000,
|
||||
service_interval_km=15000,
|
||||
assigned_servicer=self.servicer
|
||||
)
|
||||
|
||||
def test_create_work_order_creates_notifications_for_creator_and_servicer(self):
|
||||
# Precondition: no notifications
|
||||
VehicleNotification.objects.all().delete()
|
||||
|
||||
data = {
|
||||
"vehicle": self.vehicle,
|
||||
"creator": self.creator,
|
||||
"start_mileage": 10000,
|
||||
"end_mileage": 10120,
|
||||
"purpose": "Routine visit"
|
||||
}
|
||||
|
||||
wo = VehicleService.create_work_order(data)
|
||||
|
||||
# Ensure work order created
|
||||
self.assertIsNotNone(wo.pk)
|
||||
|
||||
# Notifications: one for creator, one for assigned servicer
|
||||
creator_notifs = VehicleNotification.objects.filter(recipient=self.creator)
|
||||
servicer_notifs = VehicleNotification.objects.filter(recipient=self.servicer)
|
||||
|
||||
self.assertTrue(creator_notifs.exists(), "Creator should receive a notification")
|
||||
self.assertTrue(servicer_notifs.exists(), "Assigned servicer should receive a notification")
|
||||
|
||||
def test_create_service_record_creates_notifications_for_performer_and_assigned_servicer(self):
|
||||
# Precondition: no notifications
|
||||
VehicleNotification.objects.all().delete()
|
||||
|
||||
data = {
|
||||
"vehicle": self.vehicle,
|
||||
"performed_by": self.technician,
|
||||
"description": "Brake replacement",
|
||||
"parts": "pads;discs",
|
||||
"cost": "200.00",
|
||||
"mileage": 10200
|
||||
}
|
||||
|
||||
rec = VehicleService.create_service_record(data)
|
||||
|
||||
# Ensure service record created
|
||||
self.assertIsNotNone(rec.pk)
|
||||
|
||||
# There should be a notification for the performer
|
||||
perf_notifs = VehicleNotification.objects.filter(recipient=self.technician)
|
||||
self.assertTrue(perf_notifs.exists(), "Performer should receive a notification")
|
||||
|
||||
# There should be a notification for the assigned servicer (different from performer)
|
||||
servicer_notifs = VehicleNotification.objects.filter(recipient=self.servicer)
|
||||
self.assertTrue(servicer_notifs.exists(), "Assigned servicer should receive a notification when different from performer")
|
||||
|
||||
|
||||
class DeleteWorkOrderPermissionTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.creator = User.objects.create_user(username=f"creator_{uuid.uuid4().hex[:6]}", email=f"creator+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.other = User.objects.create_user(username=f"other_{uuid.uuid4().hex[:6]}", email=f"other+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.staff = User.objects.create_user(username=f"admin_{uuid.uuid4().hex[:6]}", email=f"admin+{uuid.uuid4()}@example.test", password="pass", is_staff=True)
|
||||
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
registration_number=f"DEL-{uuid.uuid4().hex[:6].upper()}",
|
||||
current_mileage=5000
|
||||
)
|
||||
|
||||
self.wo = WorkOrder.objects.create(vehicle=self.vehicle, creator=self.creator, start_mileage=5000)
|
||||
|
||||
def test_creator_can_delete_work_order_and_notification_created(self):
|
||||
# Ensure active initially
|
||||
self.wo.refresh_from_db()
|
||||
self.assertTrue(self.wo.is_active)
|
||||
|
||||
# Delete by creator
|
||||
VehicleService.delete_work_order(self.wo, user=self.creator)
|
||||
|
||||
# Refresh and assert deactivated
|
||||
self.wo.refresh_from_db()
|
||||
self.assertFalse(self.wo.is_active)
|
||||
|
||||
# Notification exists for creator
|
||||
notif_exists = VehicleNotification.objects.filter(recipient=self.creator, title__icontains="obrisan").exists()
|
||||
self.assertTrue(notif_exists)
|
||||
|
||||
def test_non_creator_cannot_delete_work_order(self):
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleService.delete_work_order(self.wo, user=self.other)
|
||||
|
||||
def test_staff_can_delete_work_order(self):
|
||||
# staff should be allowed
|
||||
VehicleService.delete_work_order(self.wo, user=self.staff)
|
||||
self.wo.refresh_from_db()
|
||||
self.assertFalse(self.wo.is_active)
|
||||
# Notification exists for creator as well (as per implementation)
|
||||
self.assertTrue(VehicleNotification.objects.filter(recipient=self.creator, title__icontains="obrisan").exists())
|
||||
362
backend/modules/fleet/tests/test_photo_services.py
Normal file
362
backend/modules/fleet/tests/test_photo_services.py
Normal file
@@ -0,0 +1,362 @@
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from django.test import TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from PIL import Image
|
||||
|
||||
from modules.fleet.models import Vehicle, VehicleServiceRecord, VehicleServicePhoto
|
||||
from modules.fleet.services import VehicleServicePhotoService
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def create_test_image(filename="test.jpg", size=(100, 100), color='red'):
|
||||
"""
|
||||
Stvori test sliku u memoriji koristeći Pillow.
|
||||
Vraća SimpleUploadedFile spreman za upload.
|
||||
"""
|
||||
file = BytesIO()
|
||||
image = Image.new('RGB', size=size, color=color)
|
||||
image.save(file, 'JPEG')
|
||||
file.seek(0)
|
||||
return SimpleUploadedFile(
|
||||
name=filename,
|
||||
content=file.getvalue(),
|
||||
content_type='image/jpeg'
|
||||
)
|
||||
|
||||
|
||||
class VehicleServicePhotoServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
# Create users
|
||||
self.uploader = User.objects.create_user(
|
||||
username=f"uploader_{uuid.uuid4().hex[:6]}",
|
||||
email=f"uploader+{uuid.uuid4()}@example.test",
|
||||
password="pass"
|
||||
)
|
||||
self.other_user = User.objects.create_user(
|
||||
username=f"other_{uuid.uuid4().hex[:6]}",
|
||||
email=f"other+{uuid.uuid4()}@example.test",
|
||||
password="pass"
|
||||
)
|
||||
self.staff_user = User.objects.create_user(
|
||||
username=f"admin_{uuid.uuid4().hex[:6]}",
|
||||
email=f"admin+{uuid.uuid4()}@example.test",
|
||||
password="pass",
|
||||
is_staff=True
|
||||
)
|
||||
|
||||
# Create vehicle and service record
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
registration_number=f"SRVPHOTO-{uuid.uuid4().hex[:6].upper()}",
|
||||
make="TestMake",
|
||||
model="TestModel",
|
||||
current_mileage=5000,
|
||||
service_interval_km=10000
|
||||
)
|
||||
|
||||
self.service_record = VehicleServiceRecord.objects.create(
|
||||
vehicle=self.vehicle,
|
||||
performed_by=self.uploader,
|
||||
description="Test service",
|
||||
mileage=5000,
|
||||
parts="part1;part2"
|
||||
)
|
||||
|
||||
def test_upload_photo_success(self):
|
||||
"""
|
||||
Test uspješnog uploada fotografije s valjanim podacima.
|
||||
"""
|
||||
image = create_test_image(filename="success.jpg")
|
||||
|
||||
data = {
|
||||
'service_record': self.service_record,
|
||||
'image': image,
|
||||
'description': 'Test brake replacement'
|
||||
}
|
||||
|
||||
photo = VehicleServicePhotoService.upload_photo(data=data, uploaded_by=self.uploader)
|
||||
|
||||
self.assertIsNotNone(photo.pk)
|
||||
self.assertEqual(photo.uploaded_by, self.uploader)
|
||||
self.assertEqual(photo.service_record, self.service_record)
|
||||
self.assertEqual(photo.description, 'Test brake replacement')
|
||||
self.assertTrue(photo.image)
|
||||
|
||||
def test_upload_photo_with_service_record_id(self):
|
||||
"""
|
||||
Test uploada koristeći service_record ID umjesto instancije.
|
||||
"""
|
||||
image = create_test_image(filename="by_id.jpg")
|
||||
|
||||
data = {
|
||||
'service_record': self.service_record.id, # ID, ne instanca
|
||||
'image': image,
|
||||
'description': 'Upload by ID'
|
||||
}
|
||||
|
||||
photo = VehicleServicePhotoService.upload_photo(data=data, uploaded_by=self.uploader)
|
||||
|
||||
self.assertIsNotNone(photo.pk)
|
||||
self.assertEqual(photo.service_record, self.service_record)
|
||||
|
||||
def test_upload_photo_without_user_raises(self):
|
||||
"""
|
||||
Test — upload bez korisnika (uploaded_by) baca ValidationError.
|
||||
"""
|
||||
image = create_test_image(filename="no_user.jpg")
|
||||
|
||||
data = {
|
||||
'service_record': self.service_record,
|
||||
'image': image,
|
||||
'description': 'No user'
|
||||
}
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.upload_photo(data=data, uploaded_by=None)
|
||||
|
||||
def test_upload_photo_invalid_service_record_raises(self):
|
||||
"""
|
||||
Test — upload s nepostojeću service_record ID baca ValidationError.
|
||||
"""
|
||||
image = create_test_image(filename="invalid_sr.jpg")
|
||||
|
||||
data = {
|
||||
'service_record': 99999, # ne postoji
|
||||
'image': image,
|
||||
'description': 'Invalid SR'
|
||||
}
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.upload_photo(data=data, uploaded_by=self.uploader)
|
||||
|
||||
def test_update_photo_by_uploader_success(self):
|
||||
"""
|
||||
Test ažuriranja fotografije od strane korisnika koji ju je uploadao.
|
||||
"""
|
||||
image = create_test_image(filename="original.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original description",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
update_data = {'description': 'Updated description'}
|
||||
updated_photo = VehicleServicePhotoService.update_photo(
|
||||
instance=photo,
|
||||
data=update_data,
|
||||
user=self.uploader
|
||||
)
|
||||
|
||||
self.assertEqual(updated_photo.description, 'Updated description')
|
||||
photo.refresh_from_db()
|
||||
self.assertEqual(photo.description, 'Updated description')
|
||||
|
||||
def test_update_photo_by_staff_success(self):
|
||||
"""
|
||||
Test ažuriranja fotografije od strane staff korisnika (može ažurirati bilo koju).
|
||||
"""
|
||||
image = create_test_image(filename="staff_update.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
update_data = {'description': 'Staff updated'}
|
||||
updated_photo = VehicleServicePhotoService.update_photo(
|
||||
instance=photo,
|
||||
data=update_data,
|
||||
user=self.staff_user
|
||||
)
|
||||
|
||||
self.assertEqual(updated_photo.description, 'Staff updated')
|
||||
|
||||
def test_update_photo_by_non_uploader_denied(self):
|
||||
"""
|
||||
Test — korisnik koji nije uploader niti staff ne može ažurirati.
|
||||
"""
|
||||
image = create_test_image(filename="denied_update.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
update_data = {'description': 'Hacker attempt'}
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.update_photo(
|
||||
instance=photo,
|
||||
data=update_data,
|
||||
user=self.other_user
|
||||
)
|
||||
|
||||
def test_update_photo_without_user_denied(self):
|
||||
"""
|
||||
Test — ažuriranje bez korisnika (user=None) baca ValidationError.
|
||||
"""
|
||||
image = create_test_image(filename="no_user_update.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
update_data = {'description': 'No auth'}
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.update_photo(
|
||||
instance=photo,
|
||||
data=update_data,
|
||||
user=None
|
||||
)
|
||||
|
||||
def test_delete_photo_by_uploader_success(self):
|
||||
"""
|
||||
Test soft-delete fotografije od strane korisnika koji ju je uploadao.
|
||||
"""
|
||||
image = create_test_image(filename="delete.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="To delete",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
self.assertTrue(photo.is_active)
|
||||
|
||||
deleted_photo = VehicleServicePhotoService.delete_photo(
|
||||
instance=photo,
|
||||
user=self.uploader
|
||||
)
|
||||
|
||||
self.assertFalse(deleted_photo.is_active)
|
||||
photo.refresh_from_db()
|
||||
self.assertFalse(photo.is_active)
|
||||
|
||||
def test_delete_photo_by_staff_success(self):
|
||||
"""
|
||||
Test soft-delete od strane staff korisnika.
|
||||
"""
|
||||
image = create_test_image(filename="staff_delete.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="To delete",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
VehicleServicePhotoService.delete_photo(
|
||||
instance=photo,
|
||||
user=self.staff_user
|
||||
)
|
||||
|
||||
photo.refresh_from_db()
|
||||
self.assertFalse(photo.is_active)
|
||||
|
||||
def test_delete_photo_by_non_uploader_denied(self):
|
||||
"""
|
||||
Test — korisnik koji nije uploader niti staff ne može obrisati.
|
||||
"""
|
||||
image = create_test_image(filename="denied_delete.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.delete_photo(
|
||||
instance=photo,
|
||||
user=self.other_user
|
||||
)
|
||||
|
||||
# Fotografija je i dalje aktivna
|
||||
photo.refresh_from_db()
|
||||
self.assertTrue(photo.is_active)
|
||||
|
||||
def test_delete_photo_without_user_denied(self):
|
||||
"""
|
||||
Test — brisanje bez korisnika (user=None) baca ValidationError.
|
||||
"""
|
||||
image = create_test_image(filename="no_user_delete.jpg")
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
with self.assertRaises(DRFValidationError):
|
||||
VehicleServicePhotoService.delete_photo(
|
||||
instance=photo,
|
||||
user=None
|
||||
)
|
||||
|
||||
photo.refresh_from_db()
|
||||
self.assertTrue(photo.is_active)
|
||||
|
||||
def test_update_photo_does_not_allow_image_field_update(self):
|
||||
"""
|
||||
Test — update ne dozvoljava ponovno upload slike (ili je ignorirano).
|
||||
Sadržaj image polja se ne mijenja ako je proslijeđen u update_data.
|
||||
"""
|
||||
image1 = create_test_image(filename="original.jpg", color='red')
|
||||
photo = VehicleServicePhoto.objects.create(
|
||||
service_record=self.service_record,
|
||||
image=image1,
|
||||
description="Original",
|
||||
uploaded_by=self.uploader
|
||||
)
|
||||
|
||||
original_image_name = photo.image.name
|
||||
|
||||
# Pokušaj update s novom slikom
|
||||
image2 = create_test_image(filename="new.jpg", color='blue')
|
||||
update_data = {
|
||||
'description': 'Updated',
|
||||
'image': image2
|
||||
}
|
||||
|
||||
VehicleServicePhotoService.update_photo(
|
||||
instance=photo,
|
||||
data=update_data,
|
||||
user=self.uploader
|
||||
)
|
||||
|
||||
photo.refresh_from_db()
|
||||
# image bi trebao biti preskočen (ne ažurira se)
|
||||
self.assertEqual(photo.image.name, original_image_name)
|
||||
self.assertEqual(photo.description, 'Updated')
|
||||
|
||||
def test_multiple_photos_for_same_service_record(self):
|
||||
"""
|
||||
Test — isti servisni zapis može imati više fotografija.
|
||||
"""
|
||||
image1 = create_test_image(filename="photo1.jpg", color='red')
|
||||
image2 = create_test_image(filename="photo2.jpg", color='blue')
|
||||
image3 = create_test_image(filename="photo3.jpg", color='green')
|
||||
|
||||
data1 = {'service_record': self.service_record, 'image': image1, 'description': 'Photo 1'}
|
||||
data2 = {'service_record': self.service_record, 'image': image2, 'description': 'Photo 2'}
|
||||
data3 = {'service_record': self.service_record, 'image': image3, 'description': 'Photo 3'}
|
||||
|
||||
photo1 = VehicleServicePhotoService.upload_photo(data=data1, uploaded_by=self.uploader)
|
||||
photo2 = VehicleServicePhotoService.upload_photo(data=data2, uploaded_by=self.uploader)
|
||||
photo3 = VehicleServicePhotoService.upload_photo(data=data3, uploaded_by=self.other_user)
|
||||
|
||||
photos = VehicleServicePhoto.objects.filter(service_record=self.service_record, is_active=True)
|
||||
self.assertEqual(photos.count(), 3)
|
||||
|
||||
# Provjeri da su sve aktivne
|
||||
for photo in [photo1, photo2, photo3]:
|
||||
self.assertTrue(photo.is_active)
|
||||
88
backend/modules/fleet/tests/test_pusher_service.py
Normal file
88
backend/modules/fleet/tests/test_pusher_service.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# backend/modules/fleet/tests/test_pusher_service.py
|
||||
from django.test import TestCase
|
||||
from unittest.mock import Mock, patch
|
||||
import uuid
|
||||
|
||||
from infrastructure.pusher_service import PusherService
|
||||
|
||||
|
||||
class PusherServiceTests(TestCase):
|
||||
@patch("infrastructure.pusher_service.PusherService.get_client")
|
||||
@patch("infrastructure.pusher_service.PusherService.is_available", return_value=True)
|
||||
def test_broadcast_notification_success(self, _is_available, mock_get_client):
|
||||
mock_client = Mock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_client.trigger.return_value = {"message": "ok"}
|
||||
|
||||
ok = PusherService.broadcast_notification(
|
||||
user_id=42,
|
||||
title="Test naslov",
|
||||
message="Test poruka",
|
||||
level="info",
|
||||
notification_id=123,
|
||||
)
|
||||
|
||||
self.assertTrue(ok)
|
||||
mock_client.trigger.assert_called_once()
|
||||
_, kwargs = mock_client.trigger.call_args
|
||||
self.assertEqual(kwargs["channels"], "private-user-42")
|
||||
self.assertEqual(kwargs["event_name"], "vehicle.notification.created")
|
||||
self.assertEqual(kwargs["data"]["id"], 123)
|
||||
|
||||
@patch("infrastructure.pusher_service.PusherService.get_client")
|
||||
@patch("infrastructure.pusher_service.PusherService.is_available", return_value=True)
|
||||
def test_authenticate_channel_allows_own_private_channel(self, _is_available, mock_get_client):
|
||||
mock_client = Mock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_client.authenticate.return_value = {"auth": "signed-token"}
|
||||
|
||||
auth_data = PusherService.authenticate_channel(
|
||||
socket_id="123.456",
|
||||
channel_name="private-user-7",
|
||||
user_id=7,
|
||||
)
|
||||
|
||||
self.assertEqual(auth_data, {"auth": "signed-token"})
|
||||
mock_client.authenticate.assert_called_once_with(
|
||||
channel="private-user-7",
|
||||
socket_id="123.456",
|
||||
)
|
||||
|
||||
@patch("infrastructure.pusher_service.PusherService.get_client")
|
||||
@patch("infrastructure.pusher_service.PusherService.is_available", return_value=True)
|
||||
def test_authenticate_channel_rejects_other_users_channel(self, _is_available, mock_get_client):
|
||||
mock_client = Mock()
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
auth_data = PusherService.authenticate_channel(
|
||||
socket_id="123.456",
|
||||
channel_name="private-user-999",
|
||||
user_id=7,
|
||||
)
|
||||
|
||||
self.assertIsNone(auth_data)
|
||||
mock_client.authenticate.assert_not_called()
|
||||
|
||||
@patch("infrastructure.pusher_service.PusherService.get_client")
|
||||
@patch("infrastructure.pusher_service.PusherService.is_available", return_value=True)
|
||||
def test_broadcast_notification_serializes_uuid_values(self, _is_available, mock_get_client):
|
||||
mock_client = Mock()
|
||||
mock_get_client.return_value = mock_client
|
||||
mock_client.trigger.return_value = {"message": "ok"}
|
||||
|
||||
notification_id = uuid.uuid4()
|
||||
metadata_uuid = uuid.uuid4()
|
||||
|
||||
ok = PusherService.broadcast_notification(
|
||||
user_id=10,
|
||||
title="UUID test",
|
||||
message="UUID payload",
|
||||
level="info",
|
||||
notification_id=notification_id,
|
||||
metadata={"entity_id": metadata_uuid},
|
||||
)
|
||||
|
||||
self.assertTrue(ok)
|
||||
_, kwargs = mock_client.trigger.call_args
|
||||
self.assertEqual(kwargs["data"]["id"], str(notification_id))
|
||||
self.assertEqual(kwargs["data"]["metadata"]["entity_id"], str(metadata_uuid))
|
||||
@@ -0,0 +1,17 @@
|
||||
import pytest
|
||||
from django.db import models
|
||||
from modules.fleet.models import Vehicle, VehicleServiceRecord
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_vehicle_total_cost_calculation(test_user):
|
||||
# Kreiraj vozilo prema tvom modelu
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-001")
|
||||
|
||||
VehicleServiceRecord.objects.create(vehicle=vehicle, performed_by=test_user, cost=100.00, mileage=5000, description="Servis 1")
|
||||
VehicleServiceRecord.objects.create(vehicle=vehicle, performed_by=test_user, cost=50.50, mileage=5200, description="Servis 2")
|
||||
|
||||
# Agregacija kroz tvoj related_name='service_records'
|
||||
total = vehicle.service_records.aggregate(models.Sum('cost'))['cost__sum']
|
||||
|
||||
# Decimal field vraća Decimal, pa uspoređujemo s float-om ili Decimal-om
|
||||
assert float(total) == 150.50
|
||||
22
backend/modules/fleet/tests/test_service_photo_upload_to.py
Normal file
22
backend/modules/fleet/tests/test_service_photo_upload_to.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from modules.fleet.models import _service_photo_upload_to
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_service_photo_upload_path_fallback():
|
||||
"""
|
||||
Testira fallback mehanizam: ako instanca nema service_record,
|
||||
putanja mora sadržavati 'unknown'.
|
||||
"""
|
||||
# Simuliramo instancu koja nema 'service_record' ili baca grešku
|
||||
instance = MagicMock()
|
||||
del instance.service_record # Simuliramo nedostajući atribut
|
||||
|
||||
filename = "test_kvar.jpg"
|
||||
|
||||
# Pozivamo funkciju
|
||||
path = _service_photo_upload_to(instance, filename)
|
||||
|
||||
# Očekujemo da putanja sadrži 'unknown' kao fallback
|
||||
assert "fleet/unknown/service_photos/" in path
|
||||
assert filename in path # uuid je prefiks, pa provjeravamo da li je filename na kraju
|
||||
52
backend/modules/fleet/tests/test_services.py
Normal file
52
backend/modules/fleet/tests/test_services.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import uuid
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from modules.fleet.services import VehicleService
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
||||
|
||||
|
||||
class FleetServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.user = User.objects.create_user(username="tech2", email=f"tech2+{uuid.uuid4()}@example.test", password="pass")
|
||||
self.vehicle = Vehicle.objects.create(
|
||||
registration_number=f"RI-{uuid.uuid4().hex[:6].upper()}",
|
||||
make="VW",
|
||||
model="Polo",
|
||||
current_mileage=5000,
|
||||
service_interval_km=10000,
|
||||
assigned_servicer=self.user
|
||||
)
|
||||
|
||||
def test_create_work_order_via_service(self):
|
||||
data = {"vehicle": self.vehicle, "creator": self.user, "start_mileage": 5000, "end_mileage": 5200, "purpose": "Delivery"}
|
||||
wo = VehicleService.create_work_order(data)
|
||||
self.vehicle.refresh_from_db()
|
||||
self.assertEqual(self.vehicle.current_mileage, 5200)
|
||||
self.assertEqual(wo.distance, 200)
|
||||
|
||||
def test_create_service_record_via_service(self):
|
||||
data = {"vehicle": self.vehicle, "performed_by": self.user, "description": "Brake pads", "mileage": 5200, "cost": "120.00"}
|
||||
rec = VehicleService.create_service_record(data)
|
||||
self.vehicle.refresh_from_db()
|
||||
self.assertEqual(rec.next_service_due_at, 5200 + self.vehicle.service_interval_km)
|
||||
self.assertEqual(self.vehicle.current_mileage, 5200)
|
||||
|
||||
def test_vehicle_next_service_at_logic(self):
|
||||
# current=12000, interval=15000 -> blocks=(12000//15000)+1 = 1 -> 1*15000 = 15000
|
||||
v = Vehicle.objects.create(current_mileage=12000, service_interval_km=15000)
|
||||
assert v.next_service_at() == 15000
|
||||
|
||||
# current=16000, interval=15000 -> blocks=(16000//15000)+1 = 2 -> 2*15000 = 30000
|
||||
v.current_mileage = 16000
|
||||
assert v.next_service_at() == 30000
|
||||
|
||||
def test_work_order_distance_property(self):
|
||||
vehicle = Vehicle.objects.create(registration_number="ZG-004")
|
||||
# Nalog bez završne kilometraže
|
||||
wo = WorkOrder.objects.create(vehicle=vehicle, creator=self.user, start_mileage=1000)
|
||||
assert wo.distance is None
|
||||
|
||||
# Nalog sa završnom kilometražom
|
||||
wo.end_mileage = 1100
|
||||
assert wo.distance == 100
|
||||
30
backend/modules/fleet/tests/trest_travel_expenses.py
Normal file
30
backend/modules/fleet/tests/trest_travel_expenses.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_work_order_updates_vehicle_mileage():
|
||||
# 1. Kreiraj vozilo (koristeći tvoj model 'Vehicle')
|
||||
vehicle = Vehicle.objects.create(
|
||||
registration_number="ZG-123-ABC",
|
||||
current_mileage=5000
|
||||
)
|
||||
|
||||
# 2. Kreiraj radni/putni nalog
|
||||
# Pretpostavljam da ti treba i 'creator' zbog ForeignKey relacije
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
creator = User.objects.create_user(username="testuser")
|
||||
|
||||
order = WorkOrder.objects.create(
|
||||
vehicle=vehicle,
|
||||
creator=creator,
|
||||
start_mileage=5000,
|
||||
end_mileage=5500,
|
||||
purpose="Delivery"
|
||||
)
|
||||
|
||||
# 3. Nalog je već pozvao save() pri kreiranju,
|
||||
# pa bi vozilo trebalo biti ažurirano
|
||||
vehicle.refresh_from_db()
|
||||
assert vehicle.current_mileage == 5500
|
||||
Reference in New Issue
Block a user