Skip to content

Implement comprehensive unit test coverage for backend #23

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ async def get_repository_info(
raise HTTPException(status_code=404, detail="Repository not found")

return RepositoryInfoResponse(**repo_data)
except HTTPException:
# Re-raise HTTPException without modification
raise
except RuntimeError as e:
error_msg = f"Error fetching repository info: {str(e)}"
print(error_msg)
Expand Down
29 changes: 29 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,32 @@ disallow_incomplete_defs = true
max-line-length = 88
extend-ignore = "E203"
exclude = [".git", "__pycache__", "build", "dist"]

[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra -q --tb=short"
testpaths = [
"tests",
]

[tool.coverage.run]
source = ["app"]
omit = [
"*/tests/*",
"*/venv/*",
"*/__pycache__/*",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
133 changes: 133 additions & 0 deletions backend/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Test Coverage Summary

This document provides an overview of the unit test coverage added to the gitagu backend.

## Test Structure

```
tests/
├── __init__.py
├── test_config.py # Configuration module tests
├── test_constants.py # Constants module tests
├── test_main.py # FastAPI endpoints tests
├── test_models/
│ ├── __init__.py
│ └── test_schemas.py # Pydantic model tests
└── test_services/
├── __init__.py
└── test_github.py # GitHub service tests
```

## Test Statistics

- **Total test files**: 5
- **Total test cases**: 62
- **Total lines of test code**: ~899 lines
- **All tests passing**: ✅

## Coverage Areas

### 1. Pydantic Models (`test_models/test_schemas.py`) - 18 tests
- ✅ RepositoryAnalysisRequest validation
- ✅ RepositoryAnalysisResponse with various scenarios
- ✅ RepositoryFileInfo with and without size
- ✅ RepositoryInfoResponse complete and minimal
- ✅ AnalysisProgressUpdate with details
- ✅ Task breakdown models (Request, Task, Response)
- ✅ Devin session models (Request, Response)
- ✅ DevinSetupCommand model

### 2. Configuration (`test_config.py`) - 5 tests
- ✅ Default configuration values
- ✅ Environment variable handling
- ✅ Legacy Azure endpoint fallback
- ✅ Azure model deployment name fallback
- ✅ CORS origins configuration

### 3. Constants (`test_constants.py`) - 5 tests
- ✅ Agent ID constants
- ✅ Legacy agent ID mapping
- ✅ Dependency files list
- ✅ Language mapping for dependency files
- ✅ Language map completeness validation

### 4. GitHub Service (`test_services/test_github.py`) - 17 tests
- ✅ `_safe_int_conversion` utility function (7 tests)
- ✅ GitHubService initialization
- ✅ GitHub client creation with/without token
- ✅ RepositoryFileInfo model integration
- ✅ Base64 encoding/decoding handling
- ✅ Dependency files processing
- ✅ GitHub URL constants
- ✅ Mock response structure validation
- ✅ README response structure
- ✅ Tree response structure

### 5. FastAPI Endpoints (`test_main.py`) - 17 tests

#### Basic Endpoints (4 tests)
- ✅ Root endpoint (`/`)
- ✅ Health check endpoint (`/health`)
- ✅ CORS headers handling
- ✅ 404 for nonexistent endpoints

#### Repository Analysis (3 tests)
- ✅ Successful analysis with mocked services
- ✅ Invalid request validation (422 error)
- ✅ Error handling during analysis

#### Repository Info (3 tests)
- ✅ Successful repository info retrieval
- ✅ Repository not found (404)
- ✅ Service error handling (500)

#### Task Breakdown (3 tests)
- ✅ Successful task breakdown
- ✅ Invalid request validation
- ✅ Service error handling

#### Devin Session (2 tests)
- ✅ Invalid request validation
- ✅ Valid request structure validation

#### Dependency Injection (2 tests)
- ✅ GitHub service creation
- ✅ Agent service function validation

## Testing Approach

### Mocking Strategy
- **External APIs**: Mocked using `unittest.mock`
- **Dependencies**: FastAPI dependency overrides for clean testing
- **Async functions**: Proper AsyncMock usage for async service methods
- **File operations**: No actual file I/O in tests

### Test Organization
- **Unit tests**: Isolated testing of individual components
- **Integration tests**: Testing of endpoint behavior with mocked dependencies
- **Validation tests**: Pydantic model validation and error handling
- **Edge cases**: NULL values, missing fields, error conditions

### Code Quality
- **No modifications to production code** except:
- Fixed HTTPException handling in repository info endpoint
- Added pytest configuration to pyproject.toml
- **Comprehensive mocking** for external dependencies
- **Clear test structure** with descriptive test names
- **Setup/teardown** properly implemented for stateful tests

## Test Execution

All tests pass successfully and can be run with:
```bash
pytest tests/ -v
```

Tests cover the core functionality of:
- Request/response validation
- Service layer interactions
- Configuration handling
- Error scenarios
- External API mocking

This provides a solid foundation for future development and ensures the reliability of the backend API.
1 change: 1 addition & 0 deletions backend/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tests package
81 changes: 81 additions & 0 deletions backend/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Tests for the configuration module."""
import os
import pytest
from unittest.mock import patch


class TestConfig:
"""Test configuration loading and defaults."""

@patch.dict(os.environ, {}, clear=True)
def test_default_config_values(self):
"""Test that default configuration values are set correctly."""
# Force reload the config module to pick up cleared environment
import importlib
from app import config
importlib.reload(config)

assert config.PROJECT_ENDPOINT is None
assert config.MODEL_DEPLOYMENT_NAME == "gpt-4o"
assert config.AZURE_AI_PROJECT_CONNECTION_STRING is None
assert config.AZURE_AI_AGENTS_API_KEY is None
assert config.GITHUB_TOKEN is None
assert config.GITHUB_API_URL == "https://api.github.com"
assert "http://localhost:5173" in config.CORS_ORIGINS
assert "https://gitagu.com" in config.CORS_ORIGINS

@patch.dict(os.environ, {
"PROJECT_ENDPOINT": "https://test.ai.azure.com",
"MODEL_DEPLOYMENT_NAME": "gpt-3.5-turbo",
"AZURE_AI_AGENTS_API_KEY": "test-key",
"GITHUB_TOKEN": "github-token"
})
def test_config_with_environment_variables(self):
"""Test configuration with environment variables set."""
# Force reload the config module to pick up new environment
import importlib
from app import config
importlib.reload(config)

assert config.PROJECT_ENDPOINT == "https://test.ai.azure.com"
assert config.MODEL_DEPLOYMENT_NAME == "gpt-3.5-turbo"
assert config.AZURE_AI_AGENTS_API_KEY == "test-key"
assert config.GITHUB_TOKEN == "github-token"

@patch.dict(os.environ, {
"AZURE_AI_PROJECT_CONNECTION_STRING": "https://legacy.ai.azure.com"
})
def test_legacy_azure_endpoint_fallback(self):
"""Test that legacy Azure endpoint is used as fallback."""
# Force reload the config module
import importlib
from app import config
importlib.reload(config)

assert config.AZURE_AI_PROJECT_CONNECTION_STRING == "https://legacy.ai.azure.com"

@patch.dict(os.environ, {
"AZURE_AI_MODEL_DEPLOYMENT_NAME": "custom-model"
})
def test_azure_model_deployment_name_fallback(self):
"""Test Azure model deployment name fallback."""
# Force reload the config module
import importlib
from app import config
importlib.reload(config)

assert config.MODEL_DEPLOYMENT_NAME == "custom-model"

def test_cors_origins_configuration(self):
"""Test CORS origins configuration."""
from app import config

expected_origins = [
"http://localhost:5173",
"https://gitagu.com",
"https://agunblock.com",
"*"
]

for origin in expected_origins:
assert origin in config.CORS_ORIGINS
53 changes: 53 additions & 0 deletions backend/tests/test_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Tests for the constants module."""
from app.constants import (
AGENT_ID_GITHUB_COPILOT_COMPLETIONS,
AGENT_ID_GITHUB_COPILOT_AGENT,
AGENT_ID_DEVIN,
AGENT_ID_CODEX_CLI,
AGENT_ID_SREAGENT,
LEGACY_AGENT_ID_MAP,
DEPENDENCY_FILES,
LANGUAGE_MAP,
)


class TestConstants:
"""Test application constants."""

def test_agent_ids(self):
"""Test that agent ID constants are defined correctly."""
assert AGENT_ID_GITHUB_COPILOT_COMPLETIONS == 'github-copilot-completions'
assert AGENT_ID_GITHUB_COPILOT_AGENT == 'github-copilot-agent'
assert AGENT_ID_DEVIN == 'devin'
assert AGENT_ID_CODEX_CLI == 'codex-cli'
assert AGENT_ID_SREAGENT == 'sreagent'

def test_legacy_agent_id_mapping(self):
"""Test legacy agent ID mapping."""
assert 'github-copilot' in LEGACY_AGENT_ID_MAP
assert LEGACY_AGENT_ID_MAP['github-copilot'] == AGENT_ID_GITHUB_COPILOT_COMPLETIONS

def test_dependency_files(self):
"""Test dependency files list."""
expected_files = ["requirements.txt", "package.json", "pom.xml", "build.gradle"]
assert DEPENDENCY_FILES == expected_files
assert len(DEPENDENCY_FILES) == 4

def test_language_mapping(self):
"""Test language mapping for dependency files."""
expected_mappings = {
"requirements.txt": "Python",
"package.json": "JavaScript/TypeScript",
"pom.xml": "Java",
"build.gradle": "Java/Kotlin",
}
assert LANGUAGE_MAP == expected_mappings

# Test that all dependency files have language mappings
for dep_file in DEPENDENCY_FILES:
assert dep_file in LANGUAGE_MAP

def test_language_map_completeness(self):
"""Test that all dependency files have corresponding language mappings."""
for dependency_file in DEPENDENCY_FILES:
assert dependency_file in LANGUAGE_MAP, f"Missing language mapping for {dependency_file}"
Loading