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

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 

8 

9 

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) 

16 

17 url = f"/api/v1/users/{user.id}/make-admin/" 

18 resp = client.post(url) 

19 user.refresh_from_db() 

20 

21 assert resp.status_code == 200 

22 assert user.is_staff is True 

23 

24 

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) 

31 

32 url = f"/api/v1/users/{user.id}/remove-admin/" 

33 resp = client.delete(url) 

34 user.refresh_from_db() 

35 

36 assert resp.status_code == 200 

37 assert user.is_staff is False 

38 

39 

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) 

47 

48 url = f"/api/v1/users/{user.id}/make-operator/" 

49 resp = client.post(url, {"lot_id": lot.id}, format="json") 

50 

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 

55 

56 

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) 

63 

64 client = APIClient() 

65 client.force_authenticate(admin) 

66 

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

72 

73 

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) 

79 

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"] 

88 

89 delete_resp = client.delete(f"/api/v1/lots/{lot_id}/") 

90 assert delete_resp.status_code in (204, 200) 

91 

92# ============= USER MANAGEMENT ============= 

93 

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 ) 

101 

102 client = APIClient() 

103 client.force_authenticate(user=admin) 

104 

105 response = client.get("/api/v1/users/me/") 

106 

107 assert response.status_code == status.HTTP_200_OK 

108 assert response.data["username"] == "admin" 

109 assert response.data["is_staff"] is True 

110 

111 

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 ) 

119 

120 client = APIClient() 

121 client.force_authenticate(user=admin) 

122 

123 response = client.patch( 

124 "/api/v1/users/me/", 

125 {"email": "newemail@test.com"}, 

126 format="json" 

127 ) 

128 

129 assert response.status_code == status.HTTP_200_OK 

130 assert response.data["email"] == admin.email 

131 

132 

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 ) 

139 

140 client = APIClient() 

141 client.force_authenticate(user=regular_user) 

142 

143 response = client.get("/api/v1/users/me/") 

144 

145 assert response.status_code == status.HTTP_200_OK 

146 assert response.data["is_staff"] is False 

147 

148 

149# ============= PARKING LOT MANAGEMENT ============= 

150 

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

155 

156 client = APIClient() 

157 client.force_authenticate(user=admin) 

158 

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 ) 

169 

170 assert response.status_code == status.HTTP_201_CREATED 

171 assert ParkingLot.objects.filter(name="New Parking").exists() 

172 

173 

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

178 

179 client = APIClient() 

180 client.force_authenticate(user=regular_user) 

181 

182 response = client.post( 

183 "/api/v1/lots/", 

184 { 

185 "name": "Unauthorized Lot", 

186 "city": "Kyiv", 

187 "street": "Main" 

188 }, 

189 format="json" 

190 ) 

191 

192 assert response.status_code == status.HTTP_403_FORBIDDEN 

193 

194 

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

200 

201 client = APIClient() 

202 client.force_authenticate(user=admin) 

203 

204 response = client.patch( 

205 f"/api/v1/lots/{lot.id}/", 

206 {"name": "New Name"}, 

207 format="json" 

208 ) 

209 

210 assert response.status_code == status.HTTP_200_OK 

211 lot.refresh_from_db() 

212 assert lot.name == "New Name" 

213 

214 

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

220 

221 client = APIClient() 

222 client.force_authenticate(user=admin) 

223 

224 response = client.delete(f"/api/v1/lots/{lot.id}/") 

225 

226 assert response.status_code == status.HTTP_204_NO_CONTENT 

227 assert not ParkingLot.objects.filter(id=lot.id).exists() 

228 

229 

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

234 

235 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main") 

236 spot = Spot.objects.create(number="A1", lot=lot) 

237 

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 ) 

246 

247 client = APIClient() 

248 client.force_authenticate(user=admin) 

249 

250 response = client.delete(f"/api/v1/lots/{lot.id}/") 

251 

252 assert response.status_code == status.HTTP_400_BAD_REQUEST 

253 assert "booking" in response.data["detail"].lower() 

254 

255 

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

260 

261 ParkingLot.objects.create(name="Lot1", city="Kyiv", street="Main") 

262 ParkingLot.objects.create(name="Lot2", city="Lviv", street="Other") 

263 

264 client = APIClient() 

265 client.force_authenticate(user=admin) 

266 

267 response = client.get("/api/v1/lots/") 

268 

269 assert response.status_code == status.HTTP_200_OK 

270 assert len(response.data) >= 2 

271 

272 

273# ============= SPOT MANAGEMENT ============= 

274 

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

280 

281 client = APIClient() 

282 client.force_authenticate(user=admin) 

283 

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 ) 

293 

294 assert response.status_code == status.HTTP_201_CREATED 

295 assert Spot.objects.filter(number="ADMIN1", lot=lot).exists() 

296 

297 

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

303 

304 Spot.objects.create(number="A1", lot=lot) 

305 Spot.objects.create(number="A2", lot=lot) 

306 Spot.objects.create(number="A3", lot=lot) 

307 

308 client = APIClient() 

309 client.force_authenticate(user=admin) 

310 

311 response = client.get(f"/api/v1/lots/{lot.id}/spots/") 

312 

313 assert response.status_code == status.HTTP_200_OK 

314 assert len(response.data) >= 3 

315 

316 

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) 

323 

324 client = APIClient() 

325 client.force_authenticate(user=admin) 

326 

327 response = client.delete(f"/api/v1/lots/{lot.id}/spots/{spot.id}/") 

328 

329 assert response.status_code == status.HTTP_204_NO_CONTENT 

330 assert not Spot.objects.filter(id=spot.id).exists() 

331 

332 

333# ============= OPERATOR MANAGEMENT ============= 

334 

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

341 

342 # Create operator profile directly (simulating admin action) 

343 profile = OperatorProfile.objects.create(user=user, lot=lot) 

344 

345 assert profile.user == user 

346 assert profile.lot == lot 

347 assert hasattr(user, 'operator_profile') 

348 

349 

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) 

357 

358 profile.delete() 

359 

360 assert not OperatorProfile.objects.filter(user=operator).exists() 

361 

362 operator.refresh_from_db() 

363 with pytest.raises(OperatorProfile.DoesNotExist): 

364 _ = operator.operator_profile 

365 

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

371 

372 operator = User.objects.create_user(username="op") 

373 OperatorProfile.objects.create(user=operator, lot=lot1) 

374 

375 spot2 = Spot.objects.create(number="B1", lot=lot2) 

376 client = APIClient() 

377 client.force_authenticate(user=operator) 

378 

379 response = client.patch( 

380 f"/api/v1/lots/{lot2.id}/spots/{spot2.id}/operator-update/", 

381 {"is_ev": True}, 

382 format="json" 

383 ) 

384 

385 assert response.status_code == status.HTTP_403_FORBIDDEN 

386 

387 

388# ============= BOOKING MANAGEMENT ============= 

389 

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

394 

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) 

398 

399 user = User.objects.create_user(username="user") 

400 

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 ) 

411 

412 client = APIClient() 

413 client.force_authenticate(user=admin) 

414 

415 response = client.get("/api/v1/bookings/") 

416 

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 

420 

421 

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

426 

427 lot = ParkingLot.objects.create(name="Lot", city="Kyiv", street="Main") 

428 spot = Spot.objects.create(number="A1", lot=lot) 

429 

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 ) 

436 

437 client = APIClient() 

438 client.force_authenticate(user=admin) 

439 

440 response = client.get(f"/api/v1/bookings/{booking.id}/") 

441 

442 assert response.status_code == status.HTTP_200_OK 

443 assert response.data["id"] == booking.id 

444 

445 

446# ============= VALIDATION & BUSINESS RULES ============= 

447 

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

452 

453 client = APIClient() 

454 client.force_authenticate(user=admin) 

455 

456 response = client.post( 

457 "/api/v1/lots/", 

458 { 

459 "name": "AB", 

460 "city": "Kyiv", 

461 "street": "Main" 

462 }, 

463 format="json" 

464 ) 

465 

466 assert response.status_code == status.HTTP_400_BAD_REQUEST 

467 

468 

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

474 

475 Spot.objects.create(number="A1", lot=lot) 

476 

477 client = APIClient() 

478 client.force_authenticate(user=admin) 

479 

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 ) 

490 

491 assert response.status_code == status.HTTP_400_BAD_REQUEST 

492 

493# ============= SECURITY & PERMISSIONS ============= 

494 

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 

500 

501 response = client.post( 

502 "/api/v1/lots/", 

503 { 

504 "name": "Unauthorized", 

505 "city": "Kyiv", 

506 "street": "Main" 

507 }, 

508 format="json" 

509 ) 

510 

511 assert response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN] 

512 

513 

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

518 

519 ParkingLot.objects.create(name="Public Lot", city="Kyiv", street="Main") 

520 

521 client = APIClient() 

522 client.force_authenticate(user=regular_user) 

523 

524 response = client.get("/api/v1/lots/") 

525 

526 assert response.status_code == status.HTTP_200_OK 

527 assert len(response.data) >= 1 

528 

529 

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) 

536 

537 client = APIClient() 

538 client.force_authenticate(user=regular_user) 

539 

540 response = client.delete(f"/api/v1/lots/{lot.id}/spots/{spot.id}/") 

541 

542 assert response.status_code == status.HTTP_403_FORBIDDEN 

543 

544 

545# ============= DATA INTEGRITY ============= 

546 

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

552 

553 spot1 = Spot.objects.create(number="A1", lot=lot) 

554 spot2 = Spot.objects.create(number="A2", lot=lot) 

555 

556 spot_ids = [spot1.id, spot2.id] 

557 lot.delete() 

558 

559 assert not Spot.objects.filter(id__in=spot_ids).exists() 

560 

561 

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) 

568 

569 user_id = operator.id 

570 operator.delete() 

571 assert not OperatorProfile.objects.filter(user_id=user_id).exists()