prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

View 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

View 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

View 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
"", # 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)

View 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)

View 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

View 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)

View 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())

View 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)

View 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))

View File

@@ -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

View 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

View 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

View 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