Skip to content

Commit f6fe182

Browse files
2 parents dbded28 + 1d152ff commit f6fe182

38 files changed

+1622
-146
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Install Backend Dependencies
3434
run: |
3535
python -m pip install --upgrade pip
36-
pip install -r src/ContentProcessorAPI/requirements.txt
36+
pip install -r src/ContentProcessor/requirements.txt
3737
pip install pytest-cov
3838
pip install pytest-asyncio
3939
@@ -43,7 +43,7 @@ jobs:
4343
- name: Check if Backend Test Files Exist
4444
id: check_backend_tests
4545
run: |
46-
if [ -z "$(find src/ContentProcessorAPI/app/tests -type f -name 'test_*.py')" ]; then
46+
if [ -z "$(find src/ContentProcessor/src/tests -type f -name 'test_*.py')" ]; then
4747
echo "No backend test files found, skipping backend tests."
4848
echo "skip_backend_tests=true" >> $GITHUB_ENV
4949
else
@@ -54,8 +54,8 @@ jobs:
5454
- name: Run Backend Tests with Coverage
5555
if: env.skip_backend_tests == 'false'
5656
run: |
57-
pytest src/ContentProcessorAPI/app/tests
58-
pytest --cov=. --cov-report=term-missing --cov-report=xml
57+
cd src/ContentProcessor
58+
python -m pytest -vv --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=80
5959
6060
- name: Skip Backend Tests
6161
if: env.skip_backend_tests == 'true'

src/ContentProcessor/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ dev = [
2828
"coverage>=7.6.10",
2929
"pydantic>=2.10.5",
3030
"pytest>=8.3.4",
31+
"pytest-asyncio>=0.25.3",
3132
"pytest-cov>=6.0.0",
3233
"pytest-mock>=3.14.0",
34+
"mongomock>=2.3.1",
3335
"ruff>=0.9.1",
3436
]
3537

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
azure-appconfiguration>=1.7.1
2+
azure-identity>=1.19.0
3+
azure-storage-blob>=12.24.1
4+
azure-storage-queue>=12.12.0
5+
certifi>=2024.12.14
6+
charset-normalizer>=3.4.1
7+
openai==1.65.5
8+
pandas>=2.2.3
9+
pdf2image>=1.17.0
10+
poppler-utils>=0.1.0
11+
pydantic>=2.10.5
12+
pydantic-settings>=2.7.1
13+
pymongo>=4.11.2
14+
python-dotenv>=1.0.1
15+
tiktoken>=0.9.0
16+
coverage>=7.6.10
17+
pydantic>=2.10.5
18+
pytest>=8.3.4
19+
pytest-asyncio>=0.25.3
20+
pytest-cov>=6.0.0
21+
pytest-mock>=3.14.0
22+
mongomock>=2.3.1
23+
ruff>=0.9.1
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from libs.base.application_models import ModelBaseSettings
2+
from pydantic import Field
23

34

45
class EnvConfiguration(ModelBaseSettings):
56
# APP_CONFIG_ENDPOINT
6-
app_config_endpoint: str
7+
app_config_endpoint: str = Field(default="https://example.com")

src/ContentProcessor/src/libs/process_host/handler_process_host.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ def add_handlers_as_process(
4040
}
4141
)
4242

43-
async def start_handler_processes(self):
43+
async def start_handler_processes(self, test_mode: bool = False):
4444
for handler in self.handlers:
4545
handler["handler_info"].handler.start()
4646

47-
while True:
47+
while not test_mode:
4848
for handler in self.handlers:
4949
handler["handler_info"].handler.join(timeout=1)
5050
if (

src/ContentProcessor/src/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _initialize_application(self):
3131
# Add Azure Credential
3232
self.application_context.set_credential(DefaultAzureCredential())
3333

34-
async def run(self):
34+
async def run(self, test_mode: bool = False):
3535
# Get Process lists from the configuration - ex. ["extract", "transform", "evaluate", "save", "custom1", "custom2"....]
3636
steps = self.application_context.configuration.app_process_steps
3737

@@ -53,7 +53,7 @@ async def run(self):
5353
)
5454

5555
# Start All registered processes
56-
await handler_host_manager.start_handler_processes()
56+
await handler_host_manager.start_handler_processes(test_mode)
5757

5858

5959
async def main():
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import pytest
2+
from libs.azure_helper.comsos_mongo import CosmosMongDBHelper
3+
import mongomock
4+
5+
6+
@pytest.fixture
7+
def mock_mongo_client(monkeypatch):
8+
def mock_mongo_client_init(*args, **kwargs):
9+
return mongomock.MongoClient()
10+
11+
monkeypatch.setattr(
12+
"libs.azure_helper.comsos_mongo.MongoClient", mock_mongo_client_init
13+
)
14+
return mongomock.MongoClient()
15+
16+
17+
def test_prepare(mock_mongo_client, monkeypatch):
18+
indexes = ["field1", "field2"]
19+
helper = CosmosMongDBHelper(
20+
"connection_string", "db_name", "container_name", indexes=indexes
21+
)
22+
23+
assert helper.client is not None
24+
assert helper.db is not None
25+
assert helper.container is not None
26+
monkeypatch.setattr(helper.container, "index_information", lambda: indexes)
27+
helper._create_indexes(helper.container, indexes)
28+
index_info = helper.container.index_information()
29+
for index in indexes:
30+
assert f"{index}" in index_info
31+
32+
33+
def test_insert_document(mock_mongo_client):
34+
helper = CosmosMongDBHelper("connection_string", "db_name", "container_name")
35+
36+
document = {"key": "value"}
37+
helper.insert_document(document)
38+
39+
assert helper.container.find_one(document) is not None
40+
41+
42+
def test_find_document(mock_mongo_client):
43+
helper = CosmosMongDBHelper("connection_string", "db_name", "container_name")
44+
45+
query = {"key": "value"}
46+
helper.insert_document(query)
47+
result = helper.find_document(query)
48+
49+
assert len(result) == 1
50+
assert result[0] == query
51+
52+
53+
def test_find_document_with_sort(mock_mongo_client):
54+
helper = CosmosMongDBHelper("connection_string", "db_name", "container_name")
55+
56+
documents = [{"key": "value1", "sort_field": 2}, {"key": "value2", "sort_field": 1}]
57+
for doc in documents:
58+
helper.insert_document(doc)
59+
60+
query = {}
61+
sort_fields = [("sort_field", 1)]
62+
result = helper.find_document(query, sort_fields)
63+
64+
assert len(result) == 2
65+
assert result[0]["key"] == "value2"
66+
assert result[1]["key"] == "value1"
67+
68+
69+
def test_update_document(mock_mongo_client):
70+
helper = CosmosMongDBHelper("connection_string", "db_name", "container_name")
71+
72+
filter = {"key": "value"}
73+
update = {"key": "new_value"}
74+
helper.insert_document(filter)
75+
helper.update_document(filter, update)
76+
77+
result = helper.find_document(update)
78+
assert len(result) == 1
79+
assert result[0]["key"] == "new_value"
80+
81+
82+
def test_delete_document(mock_mongo_client):
83+
helper = CosmosMongDBHelper("connection_string", "db_name", "container_name")
84+
85+
helper.insert_document({"Id": "123"})
86+
helper.delete_document("123")
87+
88+
result = helper.find_document({"Id": "123"})
89+
assert len(result) == 0
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import pytest
2+
from io import BytesIO
3+
from libs.azure_helper.storage_blob import StorageBlobHelper
4+
5+
6+
@pytest.fixture
7+
def mock_blob_service_client(mocker):
8+
return mocker.patch("libs.azure_helper.storage_blob.BlobServiceClient")
9+
10+
11+
@pytest.fixture
12+
def mock_default_azure_credential(mocker):
13+
return mocker.patch("libs.azure_helper.storage_blob.DefaultAzureCredential")
14+
15+
16+
@pytest.fixture
17+
def storage_blob_helper(mock_blob_service_client, mock_default_azure_credential):
18+
return StorageBlobHelper(
19+
account_url="https://testaccount.blob.core.windows.net",
20+
container_name="testcontainer",
21+
)
22+
23+
24+
def test_get_container_client_with_parent_container(
25+
storage_blob_helper, mock_blob_service_client, mocker
26+
):
27+
mock_container_client = mocker.MagicMock()
28+
mock_blob_service_client.return_value.get_container_client.return_value = (
29+
mock_container_client
30+
)
31+
32+
# Reset call count before the specific action
33+
mock_blob_service_client.return_value.get_container_client.reset_mock()
34+
35+
# Call _get_container_client without passing container_name
36+
container_client = storage_blob_helper._get_container_client()
37+
38+
assert container_client == mock_container_client
39+
assert mock_blob_service_client.return_value.get_container_client.call_count == 1
40+
mock_blob_service_client.return_value.get_container_client.assert_called_once_with(
41+
"testcontainer"
42+
)
43+
44+
45+
def test_get_container_client_without_container_name(storage_blob_helper):
46+
storage_blob_helper.parent_container_name = None
47+
48+
with pytest.raises(
49+
ValueError,
50+
match="Container name must be provided either during initialization or as a function argument.",
51+
):
52+
storage_blob_helper._get_container_client()
53+
54+
55+
def test_upload_file(storage_blob_helper, mock_blob_service_client, mocker):
56+
mock_blob_client = mocker.MagicMock()
57+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
58+
mock_blob_client
59+
)
60+
61+
# Mock the open function to simulate reading a file
62+
mocker.patch("builtins.open", mocker.mock_open(read_data="test content"))
63+
64+
storage_blob_helper.upload_file("testcontainer", "testblob", "testfile.txt")
65+
66+
mock_blob_client.upload_blob.assert_called_once()
67+
68+
69+
def test_upload_stream(storage_blob_helper, mock_blob_service_client, mocker):
70+
mock_blob_client = mocker.MagicMock()
71+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
72+
mock_blob_client
73+
)
74+
stream = BytesIO(b"test data")
75+
76+
storage_blob_helper.upload_stream("testcontainer", "testblob", stream)
77+
78+
mock_blob_client.upload_blob.assert_called_once_with(stream, overwrite=True)
79+
80+
81+
def test_upload_text(storage_blob_helper, mock_blob_service_client, mocker):
82+
mock_blob_client = mocker.MagicMock()
83+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
84+
mock_blob_client
85+
)
86+
87+
storage_blob_helper.upload_text("testcontainer", "testblob", "test text")
88+
89+
mock_blob_client.upload_blob.assert_called_once_with("test text", overwrite=True)
90+
91+
92+
def test_download_file(storage_blob_helper, mock_blob_service_client, mocker):
93+
mock_blob_client = mocker.MagicMock()
94+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
95+
mock_blob_client
96+
)
97+
mock_blob_client.download_blob.return_value.readall.return_value = b"test data"
98+
99+
mock_open = mocker.patch("builtins.open", mocker.mock_open())
100+
storage_blob_helper.download_file("testcontainer", "testblob", "downloaded.txt")
101+
mock_open.return_value.write.assert_called_once_with(b"test data")
102+
103+
104+
def test_download_stream(storage_blob_helper, mock_blob_service_client, mocker):
105+
mock_blob_client = mocker.MagicMock()
106+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
107+
mock_blob_client
108+
)
109+
mock_blob_client.download_blob.return_value.readall.return_value = b"test data"
110+
111+
stream = storage_blob_helper.download_stream("testcontainer", "testblob")
112+
113+
assert stream == b"test data"
114+
115+
116+
def test_download_text(storage_blob_helper, mock_blob_service_client, mocker):
117+
mock_blob_client = mocker.MagicMock()
118+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
119+
mock_blob_client
120+
)
121+
mock_blob_client.download_blob.return_value.content_as_text.return_value = (
122+
"test text"
123+
)
124+
125+
text = storage_blob_helper.download_text("testcontainer", "testblob")
126+
127+
assert text == "test text"
128+
129+
130+
def test_delete_blob(storage_blob_helper, mock_blob_service_client, mocker):
131+
mock_blob_client = mocker.MagicMock()
132+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
133+
mock_blob_client
134+
)
135+
136+
storage_blob_helper.delete_blob("testcontainer", "testblob")
137+
138+
mock_blob_client.delete_blob.assert_called_once()
139+
140+
141+
def test_upload_blob_with_str(storage_blob_helper, mock_blob_service_client, mocker):
142+
mock_blob_client = mocker.MagicMock()
143+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
144+
mock_blob_client
145+
)
146+
147+
storage_blob_helper.upload_blob("testcontainer", "testblob", "test string data")
148+
149+
mock_blob_client.upload_blob.assert_called_once_with(
150+
"test string data", overwrite=True
151+
)
152+
153+
154+
def test_upload_blob_with_bytes(storage_blob_helper, mock_blob_service_client, mocker):
155+
mock_blob_client = mocker.MagicMock()
156+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
157+
mock_blob_client
158+
)
159+
160+
storage_blob_helper.upload_blob("testcontainer", "testblob", b"test bytes data")
161+
162+
mock_blob_client.upload_blob.assert_called_once_with(
163+
b"test bytes data", overwrite=True
164+
)
165+
166+
167+
def test_upload_blob_with_io(storage_blob_helper, mock_blob_service_client, mocker):
168+
mock_blob_client = mocker.MagicMock()
169+
mock_blob_service_client.return_value.get_container_client.return_value.get_blob_client.return_value = (
170+
mock_blob_client
171+
)
172+
stream = BytesIO(b"test stream data")
173+
174+
storage_blob_helper.upload_blob("testcontainer", "testblob", stream)
175+
176+
mock_blob_client.upload_blob.assert_called_once_with(stream, overwrite=True)
177+
178+
179+
def test_upload_blob_with_unsupported_type(storage_blob_helper):
180+
with pytest.raises(ValueError, match="Unsupported data type for upload"):
181+
storage_blob_helper.upload_blob("testcontainer", "testblob", 12345)

0 commit comments

Comments
 (0)