Skip to content

Commit fdb0ca1

Browse files
Merge pull request #84 from microsoft/psl-backend-unit-test
feat: backend unit test cases: part-I
2 parents e8a1de7 + 4cedd0f commit fdb0ca1

21 files changed

Lines changed: 2543 additions & 1191 deletions

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
- name: Run Backend Tests with Coverage
9696
if: env.skip_backend_tests == 'false'
9797
run: |
98-
cd src/tests/backend
98+
cd src
9999
pytest --cov=. --cov-report=term-missing --cov-report=xml
100100
101101

src/backend/common/database/database_base.py

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,55 @@ class DatabaseBase(ABC):
1616

1717
@abstractmethod
1818
async def initialize_cosmos(self) -> None:
19-
"""Initialize the cosmosdb client and create container if needed."""
20-
pass
19+
"""Initialize the cosmosdb client and create container if needed"""
20+
pass # pragma: no cover
2121

2222
@abstractmethod
2323
async def create_batch(self, user_id: str, batch_id: uuid.UUID) -> BatchRecord:
24-
"""Create a new conversion batch."""
25-
pass
24+
"""Create a new conversion batch"""
25+
pass # pragma: no cover
2626

2727
@abstractmethod
2828
async def get_file_logs(self, file_id: str) -> Dict:
29-
"""Retrieve all logs for a file."""
30-
pass
29+
"""Retrieve all logs for a file"""
30+
pass # pragma: no cover
3131

3232
@abstractmethod
3333
async def get_batch_from_id(self, batch_id: str) -> Dict:
34-
"""Retrieve all logs for a file."""
35-
pass
34+
"""Retrieve all logs for a file"""
35+
pass # pragma: no cover
3636

3737
@abstractmethod
3838
async def get_batch_files(self, batch_id: str) -> List[Dict]:
39-
"""Retrieve all files for a batch."""
40-
pass
39+
"""Retrieve all files for a batch"""
40+
pass # pragma: no cover
4141

4242
@abstractmethod
4343
async def delete_file_logs(self, file_id: str) -> None:
44-
"""Delete all logs for a file."""
45-
pass
44+
"""Delete all logs for a file"""
45+
pass # pragma: no cover
4646

4747
@abstractmethod
4848
async def get_user_batches(self, user_id: str) -> Dict:
49-
"""Retrieve all batches for a user."""
50-
pass
49+
"""Retrieve all batches for a user"""
50+
pass # pragma: no cover
5151

5252
@abstractmethod
5353
async def add_file(
5454
self, batch_id: uuid.UUID, file_id: uuid.UUID, file_name: str, storage_path: str
5555
) -> FileRecord:
56-
"""Add a file entry to the database."""
57-
pass
56+
"""Add a file entry to the database"""
57+
pass # pragma: no cover
5858

5959
@abstractmethod
6060
async def get_batch(self, user_id: str, batch_id: str) -> Optional[Dict]:
61-
"""Retrieve a batch and its associated files."""
62-
pass
61+
"""Retrieve a batch and its associated files"""
62+
pass # pragma: no cover
6363

6464
@abstractmethod
6565
async def get_file(self, file_id: str) -> Optional[Dict]:
66-
"""Retrieve a file entry along with its logs."""
67-
pass
66+
"""Retrieve a file entry along with its logs"""
67+
pass # pragma: no cover
6868

6969
@abstractmethod
7070
async def add_file_log(
@@ -76,39 +76,40 @@ async def add_file_log(
7676
agent_type: AgentType,
7777
author_role: AuthorRole,
7878
) -> None:
79-
"""Log a file status update."""
80-
pass
79+
"""Log a file status update"""
80+
pass # pragma: no cover
8181

8282
@abstractmethod
8383
async def update_file(self, file_record: FileRecord) -> None:
84-
"""Update file record."""
85-
pass
84+
"""Update file record"""
85+
pass # pragma: no cover
8686

8787
@abstractmethod
8888
async def update_batch(self, batch_record: BatchRecord) -> BatchRecord:
8989
"""Update a batch record"""
90+
pass # pragma: no cover
9091

9192
@abstractmethod
9293
async def delete_all(self, user_id: str) -> None:
93-
"""Delete all batches, files, and logs for a user."""
94-
pass
94+
"""Delete all batches, files, and logs for a user"""
95+
pass # pragma: no cover
9596

9697
@abstractmethod
9798
async def delete_batch(self, user_id: str, batch_id: str) -> None:
98-
"""Delete a batch along with its files and logs."""
99-
pass
99+
"""Delete a batch along with its files and logs"""
100+
pass # pragma: no cover
100101

101102
@abstractmethod
102103
async def delete_file(self, user_id: str, batch_id: str, file_id: str) -> None:
103-
"""Delete a file and its logs, and update batch file count."""
104-
pass
104+
"""Delete a file and its logs, and update batch file count"""
105+
pass # pragma: no cover
105106

106107
@abstractmethod
107108
async def get_batch_history(self, user_id: str, batch_id: str) -> List[Dict]:
108-
"""Retrieve all logs for a batch."""
109-
pass
109+
"""Retrieve all logs for a batch"""
110+
pass # pragma: no cover
110111

111112
@abstractmethod
112113
async def close(self) -> None:
113-
"""Close database connection."""
114-
pass
114+
"""Close database connection"""
115+
pass # pragma: no cover

src/backend/common/database/database_factory.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from typing import Optional
23

34
from common.config.config import Config
@@ -33,25 +34,20 @@ async def get_database():
3334
# Note that you have to assign yourself data plane access to Cosmos in script for this to work locally. See
3435
# https://learn.microsoft.com/en-us/azure/cosmos-db/table/security/how-to-grant-data-plane-role-based-access?tabs=built-in-definition%2Ccsharp&pivots=azure-interface-cli
3536
# Note that your principal id is your entra object id for your user account.
36-
if __name__ == "__main__":
37-
# Example usage
38-
import asyncio
39-
40-
async def main():
41-
database = await DatabaseFactory.get_database()
42-
# Use the database instance...
43-
await database.initialize_cosmos()
44-
await database.create_batch("mark1", "123e4567-e89b-12d3-a456-426614174000")
45-
await database.add_file(
46-
"123e4567-e89b-12d3-a456-426614174000",
47-
"123e4567-e89b-12d3-a456-426614174001",
48-
"q1_informix.sql",
49-
"https://cmsamarktaylstor.blob.core.windows.net/cmsablob",
50-
)
51-
tstbatch = await database.get_batch(
52-
"mark1", "123e4567-e89b-12d3-a456-426614174000"
53-
)
54-
print(tstbatch)
55-
await database.close()
37+
async def main():
38+
database = await DatabaseFactory.get_database()
39+
await database.initialize_cosmos()
40+
await database.create_batch("mark1", "123e4567-e89b-12d3-a456-426614174000")
41+
await database.add_file(
42+
"123e4567-e89b-12d3-a456-426614174000",
43+
"123e4567-e89b-12d3-a456-426614174001",
44+
"q1_informix.sql",
45+
"https://cmsamarktaylstor.blob.core.windows.net/cmsablob",
46+
)
47+
tstbatch = await database.get_batch("mark1", "123e4567-e89b-12d3-a456-426614174000")
48+
print(tstbatch)
49+
await database.close()
5650

51+
52+
if __name__ == "__main__":
5753
asyncio.run(main())

src/backend/common/storage/blob_factory.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from typing import Optional
23

34
from common.config.config import Config # Load config
@@ -31,15 +32,14 @@ async def close_storage() -> None:
3132

3233

3334
# Local testing of config and code
34-
if __name__ == "__main__":
35-
# Example usage
36-
import asyncio
35+
async def main():
36+
storage = await BlobStorageFactory.get_storage()
37+
38+
# Use the storage instance
39+
blob = await storage.get_file("q1_informix.sql")
40+
print("Blob content:", blob)
3741

38-
async def main():
39-
storage = await BlobStorageFactory.get_storage()
40-
# Use the storage instance...
41-
blob = await storage.get_file("q1_informix.sql")
42-
print(blob)
43-
await BlobStorageFactory.close_storage()
42+
await BlobStorageFactory.close_storage()
4443

44+
if __name__ == "__main__":
4545
asyncio.run(main())

src/backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ azure-functions
2020

2121
# Development tools
2222
pytest
23+
pytest-mock
2324
black
2425
pylint
2526
flake8

src/backend/sql_agents/helpers/agents_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44

5-
from semantic_kernel.agents import AzureAIAgent # pylint: disable=E0611
5+
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent # pylint: disable=E0611
66

77
from sql_agents.agents.agent_config import AgentBaseConfig
88
from sql_agents.agents.fixer.setup import setup_fixer_agent

src/backend/sql_agents/process_batch.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
from fastapi import HTTPException
2525

2626

27-
from semantic_kernel.agents import AzureAIAgent # pylint: disable=E0611
27+
from semantic_kernel.agents.azure_ai.azure_ai_agent import AzureAIAgent # pylint: disable=E0611
2828
from semantic_kernel.contents import AuthorRole
2929
from semantic_kernel.exceptions.service_exceptions import ServiceResponseException
3030

31-
3231
from sql_agents.agents.agent_config import AgentBaseConfig
3332
from sql_agents.convert_script import convert_script
3433
from sql_agents.helpers.agents_manager import SqlAgents

src/tests/backend/app_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from backend.app import create_app
2+
3+
from fastapi import FastAPI
4+
5+
from httpx import ASGITransport
6+
from httpx import AsyncClient
7+
8+
import pytest
9+
10+
11+
@pytest.fixture
12+
def app() -> FastAPI:
13+
"""Fixture to create a test app instance."""
14+
return create_app()
15+
16+
17+
@pytest.mark.asyncio
18+
async def test_health_check(app: FastAPI):
19+
"""Test the /health endpoint returns a healthy status."""
20+
transport = ASGITransport(app=app)
21+
async with AsyncClient(transport=transport, base_url="http://test") as ac:
22+
response = await ac.get("/health")
23+
assert response.status_code == 200
24+
assert response.json() == {"status": "healthy"}
25+
26+
27+
@pytest.mark.asyncio
28+
async def test_backend_routes_exist(app: FastAPI):
29+
"""Ensure /api routes are available (smoke test)."""
30+
# Check available routes include /api prefix from backend_router
31+
routes = [route.path for route in app.router.routes]
32+
backend_routes = [r for r in routes if r.startswith("/api")]
33+
assert backend_routes, "No backend routes found under /api prefix"

0 commit comments

Comments
 (0)