Skip to content
Open
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
Binary file added .coverage
Binary file not shown.
18 changes: 18 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[run]
source = app
omit =
app/migrations/*
app/conftest.py
*/tests/*
*/__pycache__/*
*/.venv/*
*/venv/*

[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError

[html]
directory = htmlcov
10 changes: 4 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ jobs:
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/testdb
run: alembic upgrade head

# 7️⃣ Run tests (code coverage step commented out)
# - name: Run tests with coverage
# working-directory: app
# env:
# DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/testdb
# run: pytest --cov=app --cov-report=xml
- name: Run tests with coverage
env:
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/testdb
run: pytest tests/ -v --cov=app --cov-report=xml --cov-report=term-missing
2 changes: 1 addition & 1 deletion app/Controllers/User/statistics_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async def root():
@router.get('/github/{userName}')
async def getGitHubData(userName: str):
logger.info("GET Request GitHub Data for user: " + userName)
return await GitHubService.getAllGitHubData(userName)
return GitHubService.getAllGitHubData(userName)

@router.get('/lc/{userName}')
async def getLeetCodeData(userName: str):
Expand Down
8 changes: 8 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[pytest]
pythonpath = app
asyncio_mode = auto
markers =
unit: Unit tests (isolated, mocked I/O).
integration: Integration tests (controller + mocked services).
e2e: End-to-end API tests (full app, mocked external HTTP).
testpaths = tests
Binary file modified requirements.txt
Binary file not shown.
Empty file added tests/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Shared pytest fixtures. App is imported with pythonpath=app (see pytest.ini).
"""
import pytest
from fastapi.testclient import TestClient

from main import app


@pytest.fixture
def client() -> TestClient:
"""FastAPI TestClient for the main app."""
return TestClient(app)
Empty file added tests/e2e/__init__.py
Empty file.
49 changes: 49 additions & 0 deletions tests/e2e/test_statistics_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""E2E API tests for statistics routes (full app, external HTTP mocked)."""
import pytest
from unittest.mock import patch, MagicMock

from fastapi.testclient import TestClient

from main import app


@pytest.fixture
def client() -> TestClient:
return TestClient(app)


@pytest.mark.e2e
class TestStatisticsApiE2E:
"""End-to-end tests for /Dijkstra/v1/statistics/*."""

def test_health_endpoint_no_mocks(self, client: TestClient):
response = client.get("/Dijkstra/v1/statistics/health")
assert response.status_code == 200
assert response.json().get("message") == "Dijkstra Statistics Health Endpoint Triggered!!!"

def test_github_endpoint_returns_structure(self, client: TestClient):
response = client.get("/Dijkstra/v1/statistics/github/anyuser")
assert response.status_code == 200
data = response.json()
assert "general_data" in data
assert "dijkstra_statistics" in data
assert "overall_github_statistics" in data

def test_lc_endpoint_with_mocked_leetcode_api(self, client: TestClient):
mock_response = MagicMock()
mock_response.json.return_value = {
"data": {
"matchedUser": {"username": "e2euser"},
"userContestRanking": None,
}
}
with patch(
"Services.User.leetcode_service.requests.post",
return_value=mock_response,
):
response = client.get("/Dijkstra/v1/statistics/lc/e2euser")

assert response.status_code == 200
data = response.json()
assert "leetcode" in data
assert data["leetcode"].get("profile", {}).get("username") == "e2euser"
Empty file added tests/integration/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions tests/integration/test_statistics_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Integration tests for statistics controller (router + mocked services)."""
import pytest
from unittest.mock import patch

from fastapi.testclient import TestClient

from main import app


@pytest.fixture
def client() -> TestClient:
return TestClient(app)


@pytest.mark.integration
class TestStatisticsController:
"""Statistics routes with patched GitHubService and LeetCodeService."""

def test_health_returns_200_and_message(self, client: TestClient):
response = client.get("/Dijkstra/v1/statistics/health")
assert response.status_code == 200
data = response.json()
assert data.get("status") == 200
assert "Dijkstra Statistics Health" in data.get("message", "")

def test_github_returns_patched_service_response(self, client: TestClient):
mock_data = {"general_data": {"username": "testuser"}, "dijkstra_statistics": {}, "overall_github_statistics": {}}
with patch(
"Controllers.User.statistics_controller.GitHubService.getAllGitHubData",
return_value=mock_data,
):
response = client.get("/Dijkstra/v1/statistics/github/testuser")

assert response.status_code == 200
assert response.json() == mock_data

def test_lc_returns_patched_service_response(self, client: TestClient):
mock_data = {"leetcode": {"profile": {"username": "lcuser"}, "contestRanking": None}}
with patch(
"Controllers.User.statistics_controller.LeetCodeService.getAllLeetcodeData",
return_value=mock_data,
):
response = client.get("/Dijkstra/v1/statistics/lc/lcuser")

assert response.status_code == 200
assert response.json() == mock_data
Empty file added tests/unit/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions tests/unit/test_github_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Unit tests for GitHubService (statistics)."""
import pytest

from Services.User.github_service import GitHubService


@pytest.mark.unit
class TestGitHubService:
"""Tests for GitHubService.getAllGitHubData."""

def test_get_all_github_data_returns_dict_with_expected_top_level_keys(self):
result = GitHubService.getAllGitHubData("johndoe")
assert isinstance(result, dict)
assert "general_data" in result
assert "dijkstra_statistics" in result
assert "overall_github_statistics" in result

def test_get_all_github_data_general_data_structure(self):
result = GitHubService.getAllGitHubData("anyuser")
general = result["general_data"]
assert "username" in general
assert "full_name" in general
assert "avatar_img_link" in general
assert "bio" in general
assert "followers" in general
assert "following" in general
assert "current_company" in general
assert "current_location" in general
assert "time_zone" in general
assert "websites_links" in general
assert "organizations_list" in general

def test_get_all_github_data_dijkstra_statistics_structure(self):
result = GitHubService.getAllGitHubData("anyuser")
dijkstra = result["dijkstra_statistics"]
assert "team" in dijkstra
assert "repositories_contributed_to" in dijkstra
assert "total_prs" in dijkstra
assert "total_lines_contributed" in dijkstra
assert "total_commits" in dijkstra
assert "dijkstra_rank" in dijkstra

def test_get_all_github_data_overall_statistics_structure(self):
result = GitHubService.getAllGitHubData("anyuser")
overall = result["overall_github_statistics"]
assert "total_lines_contributed" in overall
assert "total_prs_raised" in overall
assert "total_commits" in overall
assert "languages_used" in overall
assert "contribution_graph_link" in overall
57 changes: 57 additions & 0 deletions tests/unit/test_leetcode_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Unit tests for LeetCodeService.getAllLeetcodeData (statistics)."""
import pytest
from unittest.mock import patch, MagicMock

from Services.User.leetcode_service import LeetCodeService


@pytest.mark.unit
class TestLeetCodeServiceGetAllLeetcodeData:
"""Tests for LeetCodeService.getAllLeetcodeData."""

def test_returns_leetcode_profile_and_contest_ranking_on_success(self):
mock_response = MagicMock()
mock_response.json.return_value = {
"data": {
"matchedUser": {"username": "testuser", "profile": {}},
"userContestRanking": {"rating": 1500},
}
}
mock_response.raise_for_status = MagicMock()

with patch(
"Services.User.leetcode_service.requests.post",
return_value=mock_response,
):
result = LeetCodeService.getAllLeetcodeData("testuser")

assert "leetcode" in result
assert result["leetcode"]["profile"] == {"username": "testuser", "profile": {}}
assert result["leetcode"]["contestRanking"] == {"rating": 1500}
assert "error" not in result

def test_returns_leetcode_error_when_api_returns_errors_key(self):
mock_response = MagicMock()
mock_response.json.return_value = {
"errors": [{"message": "User not found"}]
}

with patch(
"Services.User.leetcode_service.requests.post",
return_value=mock_response,
):
result = LeetCodeService.getAllLeetcodeData("nonexistent")

assert "leetcode" in result
assert "error" in result["leetcode"]
assert result["leetcode"]["error"] == [{"message": "User not found"}]

def test_returns_error_key_on_request_exception(self):
with patch(
"Services.User.leetcode_service.requests.post",
side_effect=Exception("Connection failed"),
):
result = LeetCodeService.getAllLeetcodeData("anyuser")

assert "error" in result
assert "Connection failed" in result["error"]
Loading