Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --all-extras
- run: uv run pytest --cov --cov-report=xml
- run: uv run pytest --cov --cov-report=xml --cov-fail-under=98
- name: Upload coverage
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v7
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ Thumbs.db

# 1Password shell-plugin config: local tool state, not project config.
.op/

# Internal ticket tracker and spec; never committed.
.internal/
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ addopts = "-m 'not network'"
markers = ["network: hits the live internet; excluded from the default run"]

[tool.coverage.run]
source = ["src"]
omit = ["src/mcp_gitlab/__init__.py", "src/mcp_gitlab/__main__.py"]

[tool.mypy]
Expand Down
61 changes: 54 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,74 @@

from __future__ import annotations

from collections.abc import AsyncIterator, Iterator
from contextlib import asynccontextmanager
from typing import Any

import pytest
import respx
from fastmcp import Client, FastMCP

from mcp_gitlab.client import GitLabClient
from mcp_gitlab.config import GitLabConfig
from mcp_gitlab.servers.gitlab import mcp

TEST_URL = "https://gitlab.example.com"
TEST_TOKEN = "test-token"
API_BASE = f"{TEST_URL}/api/v4"


@pytest.fixture
def config() -> GitLabConfig:
return GitLabConfig(url=TEST_URL, token=TEST_TOKEN)
async def client() -> AsyncIterator[GitLabClient]:
"""A bare GitLabClient for client-level tests, closed after the test."""
gl = GitLabClient(GitLabConfig(url=TEST_URL, token=TEST_TOKEN))
yield gl
await gl.close()


@pytest.fixture
def client(config: GitLabConfig) -> GitLabClient:
return GitLabClient(config)
def mock_api() -> Iterator[respx.MockRouter]:
with respx.mock(base_url=API_BASE) as router:
yield router


@asynccontextmanager
async def _tool_client(*, read_only: bool) -> AsyncIterator[tuple[Client, respx.MockRouter]]:
"""The real ``mcp`` server, its lifespan swapped for one that hands tools a
GitLabClient pointed at a respx-mocked API.

The original lifespan is restored in ``finally`` so a failure inside
``Client(mcp)`` cannot leak the mock into later tests.
"""
config = GitLabConfig(url=TEST_URL, token=TEST_TOKEN, read_only=read_only)
gl = GitLabClient(config)

@asynccontextmanager
async def mock_lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]:
try:
yield {"client": gl, "config": config}
finally:
await gl.close()

original = mcp._lifespan
mcp._lifespan = mock_lifespan
try:
with respx.mock(base_url=API_BASE) as router:
async with Client(mcp) as mcp_client:
yield mcp_client, router
finally:
mcp._lifespan = original


@pytest.fixture
def mock_api() -> respx.MockRouter:
with respx.mock(base_url="https://gitlab.example.com/api/v4") as router:
yield router
async def tool_client() -> AsyncIterator[tuple[Client, respx.MockRouter]]:
"""(FastMCP client, respx router) against the server in read-write mode."""
async with _tool_client(read_only=False) as pair:
yield pair


@pytest.fixture
async def readonly_client() -> AsyncIterator[tuple[Client, respx.MockRouter]]:
"""(FastMCP client, respx router) against the server in read-only mode."""
async with _tool_client(read_only=True) as pair:
yield pair
Loading
Loading