Skip to content

Commit 6b5e5b9

Browse files
committed
feat: Add Import of FSE's
* Add new importer.py to handle imports * Add new dialog with states to go through the import process * Use a newly spawned thread to process the import async
1 parent ed15cd9 commit 6b5e5b9

File tree

19 files changed

+1299
-85
lines changed

19 files changed

+1299
-85
lines changed

backend/lcfs/db/dependencies.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import AsyncGenerator
33

44
from fastapi import Request
5-
from redis import asyncio as aioredis
65
from sqlalchemy import text
76
from sqlalchemy.engine import make_url
87
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
@@ -30,14 +29,16 @@ async def set_user_context(session: AsyncSession, username: str):
3029
raise e
3130

3231

33-
async def get_async_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]:
32+
async def get_async_db_session(
33+
request: Request,
34+
) -> AsyncGenerator[AsyncSession, None]:
3435
"""
3536
Create and get database session.
3637
:yield: database session.
3738
"""
3839
async with AsyncSession(async_engine) as session:
3940
async with session.begin():
40-
if request.user:
41+
if request and request.user:
4142
current_user_var.set(request.user)
4243
current_user = get_current_user()
4344
await set_user_context(session, current_user)

backend/lcfs/services/clamav/client.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ def scan_file(self, file):
4848
result=result,
4949
)
5050
raise VirusScanException(f"Virus detected: {result}")
51-
51+
print(result)
5252
return result
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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"]

backend/lcfs/utils/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,5 @@ class FILE_MEDIA_TYPE(Enum):
8787
}
8888

8989
default_ci = {"Gasoline": 93.67, "Diesel": 100.21, "Jet fuel": 88.83}
90+
91+
POSTAL_REGEX = r"^[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d$"

0 commit comments

Comments
 (0)