Skip to content

feat: backend unit test part II #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[run]
omit =
src/tests/backend/*
*/test_*.py
*/*_test.py
*/__init__.py

[report]
exclude_lines =
pragma: no cover
if __name__ == "__main__":
77 changes: 12 additions & 65 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
name: Test Workflow with Coverage - Code-Gen

on:
workflow_dispatch:
push:
branches:
- main
- dev
- demo
- hotfix
pull_request:
types:
- opened
Expand All @@ -16,52 +18,11 @@ on:
- main
- dev
- demo
- hotfix

jobs:
# frontend_tests:
# runs-on: ubuntu-latest

# steps:
# - name: Checkout code
# uses: actions/checkout@v3

# - name: Set up Node.js
# uses: actions/setup-node@v3
# with:
# node-version: '20'

# - name: Check if Frontend Test Files Exist
# id: check_frontend_tests
# run: |
# if [ -z "$(find src/tests/frontend -type f -name '*.test.js' -o -name '*.test.ts' -o -name '*.test.tsx')" ]; then
# echo "No frontend test files found, skipping frontend tests."
# echo "skip_frontend_tests=true" >> $GITHUB_ENV
# else
# echo "Frontend test files found, running tests."
# echo "skip_frontend_tests=false" >> $GITHUB_ENV
# fi

# - name: Install Frontend Dependencies
# if: env.skip_frontend_tests == 'false'
# run: |
# cd src/frontend
# npm install

# - name: Run Frontend Tests with Coverage
# if: env.skip_frontend_tests == 'false'
# run: |
# cd src/tests/frontend
# npm run test -- --coverage

# - name: Skip Frontend Tests
# if: env.skip_frontend_tests == 'true'
# run: |
# echo "Skipping frontend tests because no test files were found."

backend_tests:
runs-on: ubuntu-latest


steps:
- name: Checkout code
uses: actions/checkout@v3
Expand All @@ -71,36 +32,22 @@ jobs:
with:
python-version: '3.11'

- name: Install Backend Dependencies
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r src/backend/requirements.txt
pip install -r src/frontend/requirements.txt
pip install pytest-cov
pip install pytest-asyncio
pip install pytest-cov pytest-asyncio

- name: Set PYTHONPATH
run: echo "PYTHONPATH=$PWD/src/backend" >> $GITHUB_ENV

- name: Check if Backend Test Files Exist
id: check_backend_tests
run: |
if [ -z "$(find src/tests/backend -type f -name '*_test.py')" ]; then
echo "No backend test files found, skipping backend tests."
echo "skip_backend_tests=true" >> $GITHUB_ENV
else
echo "Backend test files found, running tests."
echo "skip_backend_tests=false" >> $GITHUB_ENV
fi

- name: Run Backend Tests with Coverage
if: env.skip_backend_tests == 'false'
run: |
cd src
pytest --cov=. --cov-report=term-missing --cov-report=xml



- name: Skip Backend Tests
if: env.skip_backend_tests == 'true'
run: |
echo "Skipping backend tests because no test files were found."
# only measure coverage for src/backend, omit tests via .coveragerc
pytest \
--cov=backend \
--cov-report=term-missing \
--cov-report=xml \
--cov-config=../.coveragerc
8 changes: 4 additions & 4 deletions src/backend/common/storage/blob_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def upload_file(
Returns:
Dict containing upload details (url, size, etc.)
"""
pass
pass # pragma: no cover

@abstractmethod
async def get_file(self, blob_path: str) -> BinaryIO:
Expand All @@ -38,7 +38,7 @@ async def get_file(self, blob_path: str) -> BinaryIO:
Returns:
File content as a binary stream
"""
pass
pass # pragma: no cover

@abstractmethod
async def delete_file(self, blob_path: str) -> bool:
Expand All @@ -51,7 +51,7 @@ async def delete_file(self, blob_path: str) -> bool:
Returns:
True if deletion was successful
"""
pass
pass # pragma: no cover

@abstractmethod
async def list_files(self, prefix: Optional[str] = None) -> list[Dict[str, Any]]:
Expand All @@ -64,4 +64,4 @@ async def list_files(self, prefix: Optional[str] = None) -> list[Dict[str, Any]]
Returns:
List of blob details
"""
pass
pass # pragma: no cover
2 changes: 1 addition & 1 deletion src/backend/sql_agents/convert_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
logger.setLevel(logging.DEBUG)


async def convert_script(
async def convert_script( # pragma: no cover
source_script,
file: FileRecord,
batch_service: BatchService,
Expand Down
Empty file.
83 changes: 83 additions & 0 deletions src/tests/backend/api/auth/auth_utils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import base64
import json
from unittest.mock import MagicMock

from api.auth.auth_utils import UserDetails, get_authenticated_user, get_tenant_id

from fastapi import HTTPException, Request

import pytest


def test_get_tenant_id_valid():
payload = {"tid": "tenant123"}
encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")

result = get_tenant_id(encoded)
assert result == "tenant123"


def test_get_tenant_id_invalid():
invalid_b64 = "invalid_base64_string"
result = get_tenant_id(invalid_b64)
assert result == ""


def test_user_details_initialization_with_tenant():
payload = {"tid": "tenant456"}
encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")

user_data = {
"user_principal_id": "user1",
"user_name": "John Doe",
"auth_provider": "aad",
"auth_token": "fake_token",
"client_principal_b64": encoded,
}

user = UserDetails(user_data)
assert user.user_principal_id == "user1"
assert user.user_name == "John Doe"
assert user.tenant_id == "tenant456"


def test_user_details_initialization_without_tenant():
user_data = {
"user_principal_id": "user2",
"user_name": "Jane Doe",
"auth_provider": "aad",
"auth_token": "fake_token",
"client_principal_b64": "your_base_64_encoded_token",
}

user = UserDetails(user_data)
assert user.tenant_id is None


def test_get_authenticated_user_valid():
headers = {
"x-ms-client-principal-id": "user3",
}

mock_request = MagicMock(spec=Request)
mock_request.headers = headers

user = get_authenticated_user(mock_request)
assert isinstance(user, UserDetails)
assert user.user_principal_id == "user3"


def test_get_authenticated_user_raises_http_exception(monkeypatch):
# Mocking a development environment with no user principal in sample_user
sample_user_mock = {"some-header": "some-value"}

monkeypatch.setattr("api.auth.auth_utils.sample_user", sample_user_mock)

mock_request = MagicMock(spec=Request)
mock_request.headers = {}

with pytest.raises(HTTPException) as exc_info:
get_authenticated_user(mock_request)

assert exc_info.value.status_code == 401
assert exc_info.value.detail == "User not authenticated"
139 changes: 139 additions & 0 deletions src/tests/backend/api/status_updates_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import asyncio
import uuid
from unittest.mock import AsyncMock, patch

from api import status_updates

from common.models.api import AgentType, FileProcessUpdate, FileResult, ProcessStatus

import pytest


@pytest.fixture
def file_process_update():
return FileProcessUpdate(
batch_id=uuid.uuid4(),
file_id=uuid.uuid4(),
process_status=ProcessStatus.IN_PROGRESS,
agent_type=AgentType.MIGRATOR,
agent_message="Processing in progress",
file_result=FileResult.INFO
)


@pytest.fixture
def mock_websocket():
return AsyncMock()


@pytest.mark.asyncio
async def test_send_status_update_async_success(file_process_update):
mock_websocket = AsyncMock()
status_updates.app_connection_manager.add_connection(file_process_update.batch_id, mock_websocket)

with patch("api.status_updates.json.dumps", return_value='{"batch_id": "test_batch", "status": "Processing", "progress": 50}'):
await status_updates.send_status_update_async(file_process_update)

mock_websocket.send_text.assert_awaited_once()


@pytest.mark.asyncio
async def test_send_status_update_async_no_connection(file_process_update):
# No connection added
with patch("api.status_updates.logger") as mock_logger:
await status_updates.send_status_update_async(file_process_update)
mock_logger.warning.assert_called_once_with(
"No connection found for batch ID: %s", file_process_update.batch_id
)


def test_send_status_update_success(file_process_update):
mock_websocket = AsyncMock()
loop = asyncio.new_event_loop()

with patch("api.status_updates.asyncio.get_event_loop", return_value=loop):
with patch("api.status_updates.asyncio.run_coroutine_threadsafe") as mock_run:
status_updates.app_connection_manager.add_connection(str(file_process_update.batch_id), mock_websocket)

with patch("api.status_updates.json.dumps", return_value='{}'):
status_updates.send_status_update(file_process_update)

mock_run.assert_called_once()


def test_send_status_update_no_connection(file_process_update):
with patch("api.status_updates.logger") as mock_logger:
status_updates.send_status_update(file_process_update)

mock_logger.warning.assert_called()
args, kwargs = mock_logger.warning.call_args
assert "No connection found for batch ID" in args[0]


@pytest.mark.asyncio
async def test_close_connection_success(file_process_update, mock_websocket):
status_updates.app_connection_manager.add_connection(file_process_update.batch_id, mock_websocket)
loop = asyncio.new_event_loop()

with patch("api.status_updates.asyncio.get_event_loop", return_value=loop):
with patch("api.status_updates.asyncio.run_coroutine_threadsafe") as mock_run:
with patch("api.status_updates.logger") as mock_logger:
await status_updates.close_connection(file_process_update.batch_id)

mock_run.assert_called_once()
mock_logger.info.assert_any_call("Connection closed for batch ID: %s", file_process_update.batch_id)
mock_logger.info.assert_any_call("Connection removed for batch ID: %s", file_process_update.batch_id)


@pytest.mark.asyncio
async def test_close_connection_no_connection(file_process_update):
with patch("api.status_updates.logger") as mock_logger:
await status_updates.close_connection(file_process_update.batch_id)

mock_logger.warning.assert_called_once_with(
"No connection found for batch ID: %s", file_process_update.batch_id
)
mock_logger.info.assert_called_once_with(
"Connection removed for batch ID: %s", file_process_update.batch_id
)


# Test the connection manager directly
def test_connection_manager_methods():
# Get the actual connection manager instance
manager = status_updates.app_connection_manager

# Test the get_connection method
batch_id = uuid.uuid4()
assert manager.get_connection(batch_id) is None

# Test add_connection method
mock_websocket = AsyncMock()
manager.add_connection(batch_id, mock_websocket)
assert manager.get_connection(batch_id) == mock_websocket

# Test overwriting an existing connection
new_mock_websocket = AsyncMock()
manager.add_connection(batch_id, new_mock_websocket)
assert manager.get_connection(batch_id) == new_mock_websocket

# Test remove_connection method
manager.remove_connection(batch_id)
assert manager.get_connection(batch_id) is None

# Test removing a non-existent connection (should not raise an error)
manager.remove_connection(uuid.uuid4())


def test_send_status_update_exception(file_process_update):
mock_websocket = AsyncMock()
status_updates.app_connection_manager.add_connection(str(file_process_update.batch_id), mock_websocket)

with patch("api.status_updates.asyncio.get_event_loop") as mock_loop:
mock_loop.return_value = asyncio.new_event_loop()
with patch("api.status_updates.json.dumps", return_value='{}'):
with patch("api.status_updates.asyncio.run_coroutine_threadsafe", side_effect=Exception("send error")):
with patch("api.status_updates.logger") as mock_logger:
status_updates.send_status_update(file_process_update)
mock_logger.error.assert_called_once()
assert "Failed to send message" in mock_logger.error.call_args[0][0]
Loading