|
11 | 11 | ChargingSiteCreateSchema, |
12 | 12 | ChargingSiteSchema, |
13 | 13 | ChargingSitesSchema, |
| 14 | + ChargingSiteManualStatusUpdateSchema, |
14 | 15 | ChargingEquipmentStatusSchema, |
15 | 16 | ChargingSiteStatusSchema, |
16 | 17 | ) |
@@ -383,6 +384,162 @@ async def test_update_charging_site_success(self, charging_site_service, mock_re |
383 | 384 | "Updated Site", 1, exclude_site_id=mock_existing_site.charging_site_id |
384 | 385 | ) |
385 | 386 |
|
| 387 | + @pytest.mark.anyio |
| 388 | + async def test_update_charging_site_status_manual_government_submitted_to_validated( |
| 389 | + self, charging_site_service, mock_repo, mock_request |
| 390 | + ): |
| 391 | + """Test IDIR Analyst can set status from Submitted to Validated.""" |
| 392 | + mock_request.user.organization = MagicMock() |
| 393 | + mock_request.user.organization.organization_id = 1 |
| 394 | + |
| 395 | + mock_site = MagicMock(spec=ChargingSite) |
| 396 | + mock_site.charging_site_id = 1 |
| 397 | + mock_site.organization_id = 1 |
| 398 | + mock_site.status = MagicMock() |
| 399 | + mock_site.status.status = "Submitted" |
| 400 | + |
| 401 | + mock_validated_status = MagicMock(spec=ChargingSiteStatus) |
| 402 | + mock_validated_status.charging_site_status_id = 2 |
| 403 | + mock_validated_status.status = "Validated" |
| 404 | + |
| 405 | + # Second call returns a site that must validate as ChargingSiteSchema |
| 406 | + mock_org = MagicMock() |
| 407 | + mock_org.organization_id = 1 |
| 408 | + mock_org.name = "Test Org" |
| 409 | + mock_allocating_org = MagicMock() |
| 410 | + mock_allocating_org.organization_id = 2 |
| 411 | + mock_allocating_org.name = "Allocating Org" |
| 412 | + mock_updated_site = MagicMock(spec=ChargingSite) |
| 413 | + mock_updated_site.charging_site_id = 1 |
| 414 | + mock_updated_site.group_uuid = "site-uuid-1" |
| 415 | + mock_updated_site.organization_id = 1 |
| 416 | + mock_updated_site.organization = mock_org |
| 417 | + mock_updated_site.allocating_organization_id = 2 |
| 418 | + mock_updated_site.allocating_organization = mock_allocating_org |
| 419 | + mock_updated_site.allocating_organization_name = "Allocating Org" |
| 420 | + mock_updated_site.status_id = 2 |
| 421 | + mock_updated_site.status = mock_validated_status |
| 422 | + mock_updated_site.version = 1 |
| 423 | + mock_updated_site.site_code = "SITE001" |
| 424 | + mock_updated_site.site_name = "Test Site" |
| 425 | + mock_updated_site.street_address = "123 Main St" |
| 426 | + mock_updated_site.city = "Vancouver" |
| 427 | + mock_updated_site.postal_code = "V6B 1A1" |
| 428 | + mock_updated_site.latitude = 49.2827 |
| 429 | + mock_updated_site.longitude = -123.1207 |
| 430 | + mock_updated_site.documents = [] |
| 431 | + mock_updated_site.notes = "Test notes" |
| 432 | + mock_updated_site.create_date = None |
| 433 | + mock_updated_site.update_date = None |
| 434 | + mock_updated_site.create_user = "testuser" |
| 435 | + mock_updated_site.update_user = "testuser" |
| 436 | + |
| 437 | + mock_repo.get_charging_site_by_id.return_value = mock_site |
| 438 | + mock_repo.get_charging_site_status_by_name.return_value = mock_validated_status |
| 439 | + mock_repo.update_charging_site_status.return_value = None |
| 440 | + mock_repo.get_charging_site_by_id.side_effect = [mock_site, mock_updated_site] |
| 441 | + |
| 442 | + with patch( |
| 443 | + "lcfs.web.api.charging_site.services.user_has_roles" |
| 444 | + ) as mock_has_roles: |
| 445 | + # Government and Analyst return True so IDIR Analyst path is taken |
| 446 | + mock_has_roles.side_effect = lambda user, roles: ( |
| 447 | + RoleEnum.GOVERNMENT in roles or RoleEnum.ANALYST in roles |
| 448 | + ) |
| 449 | + |
| 450 | + body = ChargingSiteManualStatusUpdateSchema(new_status="Validated") |
| 451 | + result = await charging_site_service.update_charging_site_status_manual( |
| 452 | + 1, body |
| 453 | + ) |
| 454 | + |
| 455 | + assert result.status.status == "Validated" |
| 456 | + mock_repo.update_charging_site_status.assert_called_once_with(1, 2) |
| 457 | + |
| 458 | + @pytest.mark.anyio |
| 459 | + async def test_update_charging_site_status_manual_unauthorized( |
| 460 | + self, charging_site_service, mock_repo, mock_request |
| 461 | + ): |
| 462 | + """Test user without Government or BCeID roles gets 403.""" |
| 463 | + with patch( |
| 464 | + "lcfs.web.api.charging_site.services.user_has_roles" |
| 465 | + ) as mock_has_roles: |
| 466 | + mock_has_roles.return_value = False |
| 467 | + |
| 468 | + body = ChargingSiteManualStatusUpdateSchema(new_status="Validated") |
| 469 | + with pytest.raises(HTTPException) as exc_info: |
| 470 | + await charging_site_service.update_charging_site_status_manual( |
| 471 | + 1, body |
| 472 | + ) |
| 473 | + |
| 474 | + assert exc_info.value.status_code == 403 |
| 475 | + mock_repo.get_charging_site_by_id.assert_not_called() |
| 476 | + |
| 477 | + @pytest.mark.anyio |
| 478 | + async def test_update_charging_site_status_manual_government_not_analyst_forbidden( |
| 479 | + self, charging_site_service, mock_repo, mock_request |
| 480 | + ): |
| 481 | + """Test Government user without Analyst role cannot set status to Validated (403).""" |
| 482 | + mock_request.user.organization = MagicMock() |
| 483 | + mock_request.user.organization.organization_id = 1 |
| 484 | + |
| 485 | + mock_site = MagicMock(spec=ChargingSite) |
| 486 | + mock_site.charging_site_id = 1 |
| 487 | + mock_site.organization_id = 1 |
| 488 | + mock_site.status = MagicMock() |
| 489 | + mock_site.status.status = "Submitted" |
| 490 | + |
| 491 | + mock_repo.get_charging_site_by_id.return_value = mock_site |
| 492 | + |
| 493 | + with patch( |
| 494 | + "lcfs.web.api.charging_site.services.user_has_roles" |
| 495 | + ) as mock_has_roles: |
| 496 | + # Government True, Analyst False (and not compliance/signing) |
| 497 | + mock_has_roles.side_effect = lambda user, roles: ( |
| 498 | + RoleEnum.GOVERNMENT in roles and RoleEnum.ANALYST not in roles |
| 499 | + ) |
| 500 | + |
| 501 | + body = ChargingSiteManualStatusUpdateSchema(new_status="Validated") |
| 502 | + with pytest.raises(HTTPException) as exc_info: |
| 503 | + await charging_site_service.update_charging_site_status_manual( |
| 504 | + 1, body |
| 505 | + ) |
| 506 | + |
| 507 | + assert exc_info.value.status_code == 403 |
| 508 | + assert "Only IDIR Analyst" in str(exc_info.value.detail) |
| 509 | + mock_repo.update_charging_site_status.assert_not_called() |
| 510 | + |
| 511 | + @pytest.mark.anyio |
| 512 | + async def test_update_charging_site_status_manual_invalid_transition_government( |
| 513 | + self, charging_site_service, mock_repo, mock_request |
| 514 | + ): |
| 515 | + """Test IDIR Analyst cannot set status to Validated when current is not Submitted.""" |
| 516 | + mock_request.user.organization = MagicMock() |
| 517 | + mock_request.user.organization.organization_id = 1 |
| 518 | + |
| 519 | + mock_site = MagicMock(spec=ChargingSite) |
| 520 | + mock_site.charging_site_id = 1 |
| 521 | + mock_site.organization_id = 1 |
| 522 | + mock_site.status = MagicMock() |
| 523 | + mock_site.status.status = "Draft" |
| 524 | + |
| 525 | + mock_repo.get_charging_site_by_id.return_value = mock_site |
| 526 | + |
| 527 | + with patch( |
| 528 | + "lcfs.web.api.charging_site.services.user_has_roles" |
| 529 | + ) as mock_has_roles: |
| 530 | + mock_has_roles.side_effect = lambda user, roles: ( |
| 531 | + RoleEnum.GOVERNMENT in roles or RoleEnum.ANALYST in roles |
| 532 | + ) |
| 533 | + |
| 534 | + body = ChargingSiteManualStatusUpdateSchema(new_status="Validated") |
| 535 | + with pytest.raises(HTTPException) as exc_info: |
| 536 | + await charging_site_service.update_charging_site_status_manual( |
| 537 | + 1, body |
| 538 | + ) |
| 539 | + |
| 540 | + assert exc_info.value.status_code == 400 |
| 541 | + mock_repo.update_charging_site_status.assert_not_called() |
| 542 | + |
386 | 543 | @pytest.mark.anyio |
387 | 544 | async def test_update_charging_site_validated_creates_new_version( |
388 | 545 | self, charging_site_service, mock_repo |
|
0 commit comments