diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc28210..4f12c69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,42 +1,34 @@ -# name: CI Pipeline -# on: -# workflow_dispatch: -# # push: -# # branches: -# # - main - -# jobs: -# lint-and-test: -# runs-on: ubuntu-latest - -# steps: -# - name: Checkout code -# uses: actions/checkout@v4 - -# - name: Install uv -# uses: astral-sh/setup-uv@v5 - -# - name: Set up Python -# uses: actions/setup-python@v5 -# with: -# python-version-file: .python-version - -# - name: Install dependencies -# run: | -# uv sync --all-groups - -# - name: Linting Check -# run: | -# uv run ruff check . --show-fixes - -# - name: Auto-format with Ruff -# run: | -# uv run ruff check . --fix --exit-non-zero-on-fix - -# - name: Type check with mypy -# run: uv run mypy - -# - name: Run tests -# env: -# AWS_REGION: ${{ secrets.AWS_REGION }} -# run: uv run pytest +name: CI Pipeline +on: + push: + branches: [main, feature/*] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version + + - name: Install dependencies + run: uv sync --all-groups + + - name: Linting Check + run: uv run ruff check . --show-fixes + + - name: Type check with mypy + run: uv run mypy + + - name: Run tests + run: uv run pytest --tb=short diff --git a/pyproject.toml b/pyproject.toml index 9943099..bd78ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ lint = [ "ruff>=0.14.0", ] test = [ + "httpx>=0.28.1", "pytest>=8.4.2", "pytest-asyncio>=1.2.0", ] diff --git a/tests/unit/test_additional_domain_models.py b/tests/unit/test_additional_domain_models.py new file mode 100644 index 0000000..b070677 --- /dev/null +++ b/tests/unit/test_additional_domain_models.py @@ -0,0 +1,81 @@ +"""Unit tests for additional domain models.""" + +from biomedical_graphrag.domain.citation import CitationNetwork +from biomedical_graphrag.domain.dataset import ( + GeneDataset, + GeneMetadata, + PaperDataset, + PaperMetadata, +) +from biomedical_graphrag.domain.gene import GeneRecord + + +class TestGeneRecord: + def test_default_creation(self) -> None: + gene = GeneRecord() + assert gene.gene_id == "" + assert gene.name == "" + assert gene.linked_pmids == [] + + def test_full_creation(self) -> None: + gene = GeneRecord( + gene_id="672", + name="BRCA1", + description="BRCA1 DNA repair associated", + chromosome="17", + map_location="17q21.31", + organism="Homo sapiens", + aliases="BRCA1/BRCA2-containing complex", + linked_pmids=["12345", "67890"], + ) + assert gene.gene_id == "672" + assert gene.name == "BRCA1" + assert len(gene.linked_pmids) == 2 + + +class TestCitationNetwork: + def test_default_creation(self) -> None: + citation = CitationNetwork() + assert citation.pmid == "" + assert citation.cited_by == [] + assert citation.references == [] + + def test_with_data(self) -> None: + citation = CitationNetwork( + pmid="12345", + cited_by=["11111", "22222"], + references=["33333"], + ) + assert len(citation.cited_by) == 2 + assert len(citation.references) == 1 + + +class TestDatasetModels: + def test_paper_metadata_defaults(self) -> None: + meta = PaperMetadata() + assert meta.total_papers == 0 + assert meta.collection_date == "" + + def test_paper_dataset_defaults(self) -> None: + ds = PaperDataset() + assert ds.papers == [] + assert ds.citation_network == {} + + def test_gene_metadata_defaults(self) -> None: + meta = GeneMetadata() + assert meta.total_genes == 0 + assert meta.genes_with_pubmed_links == 0 + + def test_gene_dataset_defaults(self) -> None: + ds = GeneDataset() + assert ds.genes == [] + + def test_gene_dataset_with_data(self) -> None: + gene = GeneRecord(gene_id="672", name="BRCA1") + ds = GeneDataset( + metadata=GeneMetadata(total_genes=1, genes_with_pubmed_links=1), + genes=[gene], + ) + assert ds.metadata.total_genes == 1 + assert len(ds.genes) == 1 + assert ds.genes[0].name == "BRCA1" diff --git a/tests/unit/test_api_server.py b/tests/unit/test_api_server.py new file mode 100644 index 0000000..7d49897 --- /dev/null +++ b/tests/unit/test_api_server.py @@ -0,0 +1,79 @@ +"""Unit tests for FastAPI server endpoints.""" + +import pytest +from fastapi.testclient import TestClient + +from biomedical_graphrag.api.server import ( + HealthResponse, + SearchRequest, + SearchResponse, + TraceStep, + app, +) + + +@pytest.fixture +def client(): + """Create a test client for the FastAPI app.""" + return TestClient(app) + + +class TestHealthEndpoint: + def test_health_check_returns_healthy(self, client: TestClient) -> None: + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + + +class TestSearchRequestModel: + def test_default_values(self) -> None: + req = SearchRequest(query="test") + assert req.query == "test" + assert req.limit == 5 + assert req.mode == "graphrag" + + def test_custom_values(self) -> None: + req = SearchRequest(query="BRCA1", limit=3, mode="dense") + assert req.limit == 3 + assert req.mode == "dense" + + def test_limit_validation_max(self) -> None: + from pydantic import ValidationError + + with pytest.raises(ValidationError): + SearchRequest(query="test", limit=10) + + def test_limit_validation_min(self) -> None: + from pydantic import ValidationError + + with pytest.raises(ValidationError): + SearchRequest(query="test", limit=0) + + +class TestResponseModels: + def test_search_response_defaults(self) -> None: + resp = SearchResponse() + assert resp.summary is None + assert resp.results == [] + assert resp.trace == [] + assert resp.metadata == {} + + def test_trace_step_creation(self) -> None: + step = TraceStep(name="qdrant_search", arguments={"query": "test"}, result_count=5) + assert step.name == "qdrant_search" + assert step.arguments == {"query": "test"} + assert step.result_count == 5 + + def test_health_response(self) -> None: + resp = HealthResponse() + assert resp.status == "healthy" + + +class TestSearchEndpoint: + def test_search_missing_query(self, client: TestClient) -> None: + response = client.post("/api/graphrag-query", json={}) + assert response.status_code == 422 + + def test_search_invalid_limit(self, client: TestClient) -> None: + response = client.post("/api/graphrag-query", json={"query": "test", "limit": -1}) + assert response.status_code == 422 diff --git a/uv.lock b/uv.lock index 54afc80..3288af4 100644 --- a/uv.lock +++ b/uv.lock @@ -64,6 +64,7 @@ lint = [ { name = "ruff" }, ] test = [ + { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, ] @@ -90,6 +91,7 @@ lint = [ { name = "ruff", specifier = ">=0.14.0" }, ] test = [ + { name = "httpx", specifier = ">=0.28.1" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "pytest-asyncio", specifier = ">=1.2.0" }, ]