|
| 1 | +import pytest |
| 2 | +import asyncio |
| 3 | +from unittest.mock import MagicMock, AsyncMock, patch |
| 4 | +import concurrent.futures |
| 5 | + |
| 6 | +from lcfs.web.api.final_supply_equipment.importer import FinalSupplyEquipmentImporter |
| 7 | +from lcfs.web.api.final_supply_equipment.repo import FinalSupplyEquipmentRepository |
| 8 | +from lcfs.web.api.final_supply_equipment.services import FinalSupplyEquipmentServices |
| 9 | +from lcfs.web.api.compliance_report.services import ComplianceReportServices |
| 10 | +from lcfs.services.clamav.client import ClamAVService |
| 11 | +from redis.asyncio import Redis |
| 12 | + |
| 13 | + |
| 14 | +@pytest.fixture |
| 15 | +def mock_repo() -> FinalSupplyEquipmentRepository: |
| 16 | + repo = MagicMock(spec=FinalSupplyEquipmentRepository) |
| 17 | + return repo |
| 18 | + |
| 19 | + |
| 20 | +@pytest.fixture |
| 21 | +def mock_fse_service() -> FinalSupplyEquipmentServices: |
| 22 | + service = MagicMock(spec=FinalSupplyEquipmentServices) |
| 23 | + return service |
| 24 | + |
| 25 | + |
| 26 | +@pytest.fixture |
| 27 | +def mock_compliance_service() -> ComplianceReportServices: |
| 28 | + service = MagicMock(spec=ComplianceReportServices) |
| 29 | + return service |
| 30 | + |
| 31 | + |
| 32 | +@pytest.fixture |
| 33 | +def mock_clamav() -> ClamAVService: |
| 34 | + clamav = MagicMock(spec=ClamAVService) |
| 35 | + return clamav |
| 36 | + |
| 37 | + |
| 38 | +@pytest.fixture |
| 39 | +def mock_redis() -> Redis: |
| 40 | + redis_client = MagicMock(spec=Redis) |
| 41 | + redis_client.set = AsyncMock() |
| 42 | + redis_client.get = AsyncMock() |
| 43 | + return redis_client |
| 44 | + |
| 45 | + |
| 46 | +@pytest.fixture |
| 47 | +def mock_executor(): |
| 48 | + executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) |
| 49 | + yield executor |
| 50 | + executor.shutdown(wait=False) |
| 51 | + |
| 52 | + |
| 53 | +@pytest.fixture |
| 54 | +def importer_instance( |
| 55 | + mock_repo, |
| 56 | + mock_fse_service, |
| 57 | + mock_compliance_service, |
| 58 | + mock_clamav, |
| 59 | + mock_redis, |
| 60 | + mock_executor, |
| 61 | +): |
| 62 | + """ |
| 63 | + Creates a FinalSupplyEquipmentImporter with mocked dependencies. |
| 64 | + """ |
| 65 | + return FinalSupplyEquipmentImporter( |
| 66 | + repo=mock_repo, |
| 67 | + fse_service=mock_fse_service, |
| 68 | + compliance_report_services=mock_compliance_service, |
| 69 | + clamav_service=mock_clamav, |
| 70 | + redis_client=mock_redis, |
| 71 | + executor=mock_executor, |
| 72 | + ) |
| 73 | + |
| 74 | + |
| 75 | +@pytest.mark.anyio |
| 76 | +async def test_import_data_success(importer_instance, mock_redis): |
| 77 | + file_mock = MagicMock() |
| 78 | + file_mock.filename = "test.xlsx" |
| 79 | + file_mock.read = AsyncMock(return_value=b"fake-excel-contents") |
| 80 | + |
| 81 | + user_mock = MagicMock() |
| 82 | + user_mock.organization.organization_code = "TEST-ORG" |
| 83 | + |
| 84 | + with patch( |
| 85 | + "lcfs.web.api.final_supply_equipment.importer.import_async", |
| 86 | + new=AsyncMock(return_value=None), |
| 87 | + ) as mock_import_task: |
| 88 | + |
| 89 | + job_id = await importer_instance.import_data( |
| 90 | + compliance_report_id=123, user=user_mock, file=file_mock, overwrite=False |
| 91 | + ) |
| 92 | + |
| 93 | + assert isinstance(job_id, str) |
| 94 | + assert len(job_id) > 0 |
| 95 | + |
| 96 | + # Check Redis progress was initialized |
| 97 | + mock_redis.set.assert_called() |
| 98 | + # Check our background task was scheduled |
| 99 | + mock_import_task.assert_called() |
| 100 | + |
| 101 | + |
| 102 | +@pytest.mark.anyio |
| 103 | +async def test_import_data_with_clamav(importer_instance, mock_clamav, mock_redis): |
| 104 | + with patch("lcfs.settings.settings.clamav_enabled", True): |
| 105 | + file_mock = MagicMock() |
| 106 | + file_mock.filename = "test.xlsx" |
| 107 | + file_mock.read = AsyncMock(return_value=b"excel-data") |
| 108 | + user_mock = MagicMock() |
| 109 | + |
| 110 | + with patch( |
| 111 | + "lcfs.web.api.final_supply_equipment.importer.import_async", new=AsyncMock() |
| 112 | + ) as mock_import_task: |
| 113 | + job_id = await importer_instance.import_data( |
| 114 | + compliance_report_id=999, user=user_mock, file=file_mock, overwrite=True |
| 115 | + ) |
| 116 | + |
| 117 | + assert job_id |
| 118 | + mock_import_task.assert_called() |
| 119 | + |
| 120 | + |
| 121 | +@pytest.mark.anyio |
| 122 | +async def test_get_status_no_job_found(importer_instance, mock_redis): |
| 123 | + """ |
| 124 | + Tests that get_status returns a default response if redis has no record for job_id. |
| 125 | + """ |
| 126 | + mock_redis.get = AsyncMock(return_value=None) |
| 127 | + |
| 128 | + result = await importer_instance.get_status("non-existent-id") |
| 129 | + |
| 130 | + print(result) |
| 131 | + |
| 132 | + assert result["progress"] == 0 |
| 133 | + assert "No job found" in result["status"] |
| 134 | + |
| 135 | + |
| 136 | +@pytest.mark.anyio |
| 137 | +async def test_get_status_invalid_json(importer_instance, mock_redis): |
| 138 | + """ |
| 139 | + Tests that get_status handles invalid JSON from redis gracefully. |
| 140 | + """ |
| 141 | + mock_redis.get = AsyncMock(return_value=b"not-valid-json") |
| 142 | + |
| 143 | + result = await importer_instance.get_status("corrupt-id") |
| 144 | + assert result["progress"] == 0 |
| 145 | + assert "Invalid status data found." in result["status"] |
0 commit comments