Coverage for src/api/tests/test_admin.py: 100%
290 statements
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-08 10:41 +0000
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-08 10:41 +0000
1import pytest
2from rest_framework.test import APIClient
3from django.contrib.auth.models import User
4from src.api.models import ParkingLot, OperatorProfile, Spot, Booking
5from django.utils import timezone
6from rest_framework import status
7from datetime import timedelta
10@pytest.mark.django_db
11def test_admin_can_make_other_user_admin():
12 admin = User.objects.create_user("local_admin", is_staff=True)
13 user = User.objects.create_user("simple_user")
14 client = APIClient()
15 client.force_authenticate(admin)
17 url = f"/api/v1/users/{user.id}/make-admin/"
18 resp = client.post(url)
19 user.refresh_from_db()
21 assert resp.status_code == 200
22 assert user.is_staff is True
25@pytest.mark.django_db
26def test_admin_can_remove_admin_role():
27 admin = User.objects.create_user("super", is_staff=True)
28 user = User.objects.create_user("staffer", is_staff=True)
29 client = APIClient()
30 client.force_authenticate(admin)
32 url = f"/api/v1/users/{user.id}/remove-admin/"
33 resp = client.delete(url)
34 user.refresh_from_db()
36 assert resp.status_code == 200
37 assert user.is_staff is False
40@pytest.mark.django_db
41def test_admin_can_assign_operator_to_lot():
42 admin = User.objects.create_user("admin", is_staff=True)
43 user = User.objects.create_user("new_op")
44 lot = ParkingLot.objects.create(name="Central", city="Kyiv", street="Main")
45 client = APIClient()
46 client.force_authenticate(admin)
48 url = f"/api/v1/users/{user.id}/make-operator/"
49 resp = client.post(url, {"lot_id": lot.id}, format="json")
51 user.refresh_from_db()
52 assert resp.status_code in (200, 201)
53 assert hasattr(user, "operator_profile")
54 assert user.operator_profile.lot == lot
57@pytest.mark.django_db
58def test_admin_can_remove_operator():
59 admin = User.objects.create_user("admin", is_staff=True)
60 user = User.objects.create_user("op_user")
61 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
62 OperatorProfile.objects.create(user=user, lot=lot)
64 client = APIClient()
65 client.force_authenticate(admin)
67 url = f"/api/v1/users/{user.id}/remove-operator/"
68 resp = client.delete(url)
69 assert resp.status_code == 204
70 user.refresh_from_db()
71 assert not hasattr(user, "operator_profile")
74@pytest.mark.django_db
75def test_admin_can_create_and_delete_parking_lot():
76 admin = User.objects.create_user("admin", is_staff=True)
77 client = APIClient()
78 client.force_authenticate(admin)
80 create_resp = client.post("/api/v1/lots/", {
81 "name": "MallLot",
82 "city": "Kyiv",
83 "street": "Main",
84 "building": "12A"
85 }, format="json")
86 assert create_resp.status_code == 201
87 lot_id = create_resp.data["id"]
89 delete_resp = client.delete(f"/api/v1/lots/{lot_id}/")
90 assert delete_resp.status_code in (204, 200)
92# ============= USER MANAGEMENT =============
94@pytest.mark.django_db
95def test_admin_can_view_own_profile():
96 """Admin can view their own profile via /me endpoint"""
97 admin = User.objects.create_superuser(
98 username="admin",
99 email="admin@test.com"
100 )
102 client = APIClient()
103 client.force_authenticate(user=admin)
105 response = client.get("/api/v1/users/me/")
107 assert response.status_code == status.HTTP_200_OK
108 assert response.data["username"] == "admin"
109 assert response.data["is_staff"] is True
112@pytest.mark.django_db
113def test_admin_can_update_own_profile():
114 """Admin can update their own profile information"""
115 admin = User.objects.create_superuser(
116 username="admin",
117 email="admin@test.com"
118 )
120 client = APIClient()
121 client.force_authenticate(user=admin)
123 response = client.patch(
124 "/api/v1/users/me/",
125 {"email": "newemail@test.com"},
126 format="json"
127 )
129 assert response.status_code == status.HTTP_200_OK
130 assert response.data["email"] == admin.email
133@pytest.mark.django_db
134def test_non_admin_cannot_see_admin_status():
135 """Regular user's profile doesn't show admin privileges"""
136 regular_user = User.objects.create_user(
137 username="regular"
138 )
140 client = APIClient()
141 client.force_authenticate(user=regular_user)
143 response = client.get("/api/v1/users/me/")
145 assert response.status_code == status.HTTP_200_OK
146 assert response.data["is_staff"] is False
149# ============= PARKING LOT MANAGEMENT =============
151@pytest.mark.django_db
152def test_admin_can_create_parking_lot():
153 """Admin can create a new parking lot"""
154 admin = User.objects.create_superuser(username="admin")
156 client = APIClient()
157 client.force_authenticate(user=admin)
159 response = client.post(
160 "/api/v1/lots/",
161 {
162 "name": "New Parking",
163 "city": "Kyiv",
164 "street": "Main Street",
165 "building": "10"
166 },
167 format="json"
168 )
170 assert response.status_code == status.HTTP_201_CREATED
171 assert ParkingLot.objects.filter(name="New Parking").exists()
174@pytest.mark.django_db
175def test_non_admin_cannot_create_parking_lot():
176 """Regular user cannot create parking lots"""
177 regular_user = User.objects.create_user(username="regular")
179 client = APIClient()
180 client.force_authenticate(user=regular_user)
182 response = client.post(
183 "/api/v1/lots/",
184 {
185 "name": "Unauthorized Lot",
186 "city": "Kyiv",
187 "street": "Main"
188 },
189 format="json"
190 )
192 assert response.status_code == status.HTTP_403_FORBIDDEN
195@pytest.mark.django_db
196def test_admin_can_update_parking_lot_details():
197 """Admin can update parking lot information"""
198 admin = User.objects.create_superuser(username="admin")
199 lot = ParkingLot.objects.create(name="Old Name", city="Kyiv", street="Main")
201 client = APIClient()
202 client.force_authenticate(user=admin)
204 response = client.patch(
205 f"/api/v1/lots/{lot.id}/",
206 {"name": "New Name"},
207 format="json"
208 )
210 assert response.status_code == status.HTTP_200_OK
211 lot.refresh_from_db()
212 assert lot.name == "New Name"
215@pytest.mark.django_db
216def test_admin_can_delete_empty_parking_lot():
217 """Admin can delete a parking lot without spots or bookings"""
218 admin = User.objects.create_superuser(username="admin")
219 lot = ParkingLot.objects.create(name="Empty Lot", city="Kyiv", street="Main")
221 client = APIClient()
222 client.force_authenticate(user=admin)
224 response = client.delete(f"/api/v1/lots/{lot.id}/")
226 assert response.status_code == status.HTTP_204_NO_CONTENT
227 assert not ParkingLot.objects.filter(id=lot.id).exists()
230@pytest.mark.django_db
231def test_admin_cannot_delete_lot_with_active_bookings():
232 """Admin cannot delete a parking lot that has active bookings"""
233 admin = User.objects.create_superuser(username="admin")
235 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
236 spot = Spot.objects.create(number="A1", lot=lot)
238 user = User.objects.create_user(username="user")
239 Booking.objects.create(
240 user=user,
241 spot=spot,
242 start_at=timezone.now() + timedelta(hours=1),
243 end_at=timezone.now() + timedelta(hours=2),
244 status='confirmed'
245 )
247 client = APIClient()
248 client.force_authenticate(user=admin)
250 response = client.delete(f"/api/v1/lots/{lot.id}/")
252 assert response.status_code == status.HTTP_400_BAD_REQUEST
253 assert "booking" in response.data["detail"].lower()
256@pytest.mark.django_db
257def test_admin_can_view_all_parking_lots():
258 """Admin can view list of all parking lots"""
259 admin = User.objects.create_superuser(username="admin")
261 ParkingLot.objects.create(name="Lot1", city="Kyiv", street="Main")
262 ParkingLot.objects.create(name="Lot2", city="Lviv", street="Other")
264 client = APIClient()
265 client.force_authenticate(user=admin)
267 response = client.get("/api/v1/lots/")
269 assert response.status_code == status.HTTP_200_OK
270 assert len(response.data) >= 2
273# ============= SPOT MANAGEMENT =============
275@pytest.mark.django_db
276def test_admin_can_create_spot_in_any_lot():
277 """Admin can create spots in any parking lot"""
278 admin = User.objects.create_superuser(username="admin")
279 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
281 client = APIClient()
282 client.force_authenticate(user=admin)
284 response = client.post(
285 f"/api/v1/lots/{lot.id}/spots/create/",
286 {
287 "number": "ADMIN1",
288 "is_ev": True,
289 "is_disabled": False
290 },
291 format="json"
292 )
294 assert response.status_code == status.HTTP_201_CREATED
295 assert Spot.objects.filter(number="ADMIN1", lot=lot).exists()
298@pytest.mark.django_db
299def test_admin_can_view_all_spots_in_lot():
300 """Admin can view all parking spots in a lot"""
301 admin = User.objects.create_superuser(username="admin")
302 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
304 Spot.objects.create(number="A1", lot=lot)
305 Spot.objects.create(number="A2", lot=lot)
306 Spot.objects.create(number="A3", lot=lot)
308 client = APIClient()
309 client.force_authenticate(user=admin)
311 response = client.get(f"/api/v1/lots/{lot.id}/spots/")
313 assert response.status_code == status.HTTP_200_OK
314 assert len(response.data) >= 3
317@pytest.mark.django_db
318def test_admin_can_delete_spot():
319 """Admin can delete a parking spot"""
320 admin = User.objects.create_superuser(username="admin")
321 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
322 spot = Spot.objects.create(number="DELETE_ME", lot=lot)
324 client = APIClient()
325 client.force_authenticate(user=admin)
327 response = client.delete(f"/api/v1/lots/{lot.id}/spots/{spot.id}/")
329 assert response.status_code == status.HTTP_204_NO_CONTENT
330 assert not Spot.objects.filter(id=spot.id).exists()
333# ============= OPERATOR MANAGEMENT =============
335@pytest.mark.django_db
336def test_admin_can_assign_user_as_operator():
337 """Admin can create operator profile for a user"""
338 admin = User.objects.create_superuser(username="admin")
339 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
340 user = User.objects.create_user(username="newop")
342 # Create operator profile directly (simulating admin action)
343 profile = OperatorProfile.objects.create(user=user, lot=lot)
345 assert profile.user == user
346 assert profile.lot == lot
347 assert hasattr(user, 'operator_profile')
350@pytest.mark.django_db
351def test_admin_can_remove_operator_role():
352 """Admin can remove operator profile"""
353 User.objects.create_superuser(username="admin")
354 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
355 operator = User.objects.create_user(username="op")
356 profile = OperatorProfile.objects.create(user=operator, lot=lot)
358 profile.delete()
360 assert not OperatorProfile.objects.filter(user=operator).exists()
362 operator.refresh_from_db()
363 with pytest.raises(OperatorProfile.DoesNotExist):
364 _ = operator.operator_profile
366@pytest.mark.django_db
367def test_operator_can_only_manage_assigned_lot():
368 """Operator can only access spots in their assigned lot"""
369 lot1 = ParkingLot.objects.create(name="Lot1", city="Kyiv", street="Main")
370 lot2 = ParkingLot.objects.create(name="Lot2", city="Lviv", street="Other")
372 operator = User.objects.create_user(username="op")
373 OperatorProfile.objects.create(user=operator, lot=lot1)
375 spot2 = Spot.objects.create(number="B1", lot=lot2)
376 client = APIClient()
377 client.force_authenticate(user=operator)
379 response = client.patch(
380 f"/api/v1/lots/{lot2.id}/spots/{spot2.id}/operator-update/",
381 {"is_ev": True},
382 format="json"
383 )
385 assert response.status_code == status.HTTP_403_FORBIDDEN
388# ============= BOOKING MANAGEMENT =============
390@pytest.mark.django_db
391def test_admin_can_view_all_bookings():
392 """Admin can view all bookings across all lots"""
393 admin = User.objects.create_superuser(username="admin")
395 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
396 spot1 = Spot.objects.create(number="A1", lot=lot)
397 spot2 = Spot.objects.create(number="A2", lot=lot)
399 user = User.objects.create_user(username="user")
401 Booking.objects.create(
402 user=user, spot=spot1,
403 start_at=timezone.now() + timedelta(hours=1),
404 end_at=timezone.now() + timedelta(hours=2)
405 )
406 Booking.objects.create(
407 user=user, spot=spot2,
408 start_at=timezone.now() + timedelta(hours=3),
409 end_at=timezone.now() + timedelta(hours=4)
410 )
412 client = APIClient()
413 client.force_authenticate(user=admin)
415 response = client.get("/api/v1/bookings/")
417 assert response.status_code == status.HTTP_200_OK
418 # Admin should see bookings (exact structure depends on your pagination)
419 assert len(response.data.get('results', response.data)) >= 2
422@pytest.mark.django_db
423def test_admin_can_view_specific_booking():
424 """Admin can view details of any booking"""
425 admin = User.objects.create_superuser(username="admin")
427 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
428 spot = Spot.objects.create(number="A1", lot=lot)
430 user = User.objects.create_user(username="user")
431 booking = Booking.objects.create(
432 user=user, spot=spot,
433 start_at=timezone.now() + timedelta(hours=1),
434 end_at=timezone.now() + timedelta(hours=2)
435 )
437 client = APIClient()
438 client.force_authenticate(user=admin)
440 response = client.get(f"/api/v1/bookings/{booking.id}/")
442 assert response.status_code == status.HTTP_200_OK
443 assert response.data["id"] == booking.id
446# ============= VALIDATION & BUSINESS RULES =============
448@pytest.mark.django_db
449def test_admin_must_provide_valid_lot_data():
450 """Admin must provide valid data when creating a lot"""
451 admin = User.objects.create_superuser(username="admin")
453 client = APIClient()
454 client.force_authenticate(user=admin)
456 response = client.post(
457 "/api/v1/lots/",
458 {
459 "name": "AB",
460 "city": "Kyiv",
461 "street": "Main"
462 },
463 format="json"
464 )
466 assert response.status_code == status.HTTP_400_BAD_REQUEST
469@pytest.mark.django_db
470def test_admin_cannot_create_duplicate_spot_number():
471 """Admin cannot create spots with duplicate numbers in same lot"""
472 admin = User.objects.create_superuser(username="admin")
473 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
475 Spot.objects.create(number="A1", lot=lot)
477 client = APIClient()
478 client.force_authenticate(user=admin)
480 # Try to create duplicate
481 response = client.post(
482 f"/api/v1/lots/{lot.id}/spots/create/",
483 {
484 "number": "A1",
485 "is_ev": False,
486 "is_disabled": False
487 },
488 format="json"
489 )
491 assert response.status_code == status.HTTP_400_BAD_REQUEST
493# ============= SECURITY & PERMISSIONS =============
495@pytest.mark.django_db
496def test_unauthenticated_user_cannot_create_lot():
497 """Unauthenticated users cannot create parking lots"""
498 client = APIClient()
499 # No authentication
501 response = client.post(
502 "/api/v1/lots/",
503 {
504 "name": "Unauthorized",
505 "city": "Kyiv",
506 "street": "Main"
507 },
508 format="json"
509 )
511 assert response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]
514@pytest.mark.django_db
515def test_regular_user_can_view_public_lots():
516 """Regular users can view the list of parking lots"""
517 regular_user = User.objects.create_user(username="regular")
519 ParkingLot.objects.create(name="Public Lot", city="Kyiv", street="Main")
521 client = APIClient()
522 client.force_authenticate(user=regular_user)
524 response = client.get("/api/v1/lots/")
526 assert response.status_code == status.HTTP_200_OK
527 assert len(response.data) >= 1
530@pytest.mark.django_db
531def test_regular_user_cannot_delete_spots():
532 """Regular users cannot delete parking spots"""
533 regular_user = User.objects.create_user(username="regular")
534 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
535 spot = Spot.objects.create(number="A1", lot=lot)
537 client = APIClient()
538 client.force_authenticate(user=regular_user)
540 response = client.delete(f"/api/v1/lots/{lot.id}/spots/{spot.id}/")
542 assert response.status_code == status.HTTP_403_FORBIDDEN
545# ============= DATA INTEGRITY =============
547@pytest.mark.django_db
548def test_deleting_lot_cascades_to_spots():
549 """Deleting a parking lot should cascade to its spots"""
550 User.objects.create_superuser(username="admin")
551 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
553 spot1 = Spot.objects.create(number="A1", lot=lot)
554 spot2 = Spot.objects.create(number="A2", lot=lot)
556 spot_ids = [spot1.id, spot2.id]
557 lot.delete()
559 assert not Spot.objects.filter(id__in=spot_ids).exists()
562@pytest.mark.django_db
563def test_deleting_user_cascades_to_operator_profile():
564 """Deleting a user should remove their operator profile"""
565 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main")
566 operator = User.objects.create_user(username="op")
567 OperatorProfile.objects.create(user=operator, lot=lot)
569 user_id = operator.id
570 operator.delete()
571 assert not OperatorProfile.objects.filter(user_id=user_id).exists()