Skip to content

Commit 7a2792f

Browse files
committed
Add test_cli.py and test_wrapper.py
Signed-off-by: Mihai Criveti <[email protected]>
1 parent 7b6cf8c commit 7a2792f

File tree

5 files changed

+472
-1
lines changed

5 files changed

+472
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
docs/docs/test/
12
tmp
23
*.tgz
34
*.gz

Makefile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ clean:
178178
# help: smoketest - Run smoketest.py --verbose (build container, add MCP server, test endpoints)
179179
# help: test - Run unit tests with pytest
180180
# help: coverage - Run tests with coverage, emit md/HTML/XML + badge
181+
# help: htmlcov - (re)build just the HTML coverage report into docs
181182
# help: test-curl - Smoke-test API endpoints with curl script
182183
# help: pytest-examples - Run README / examples through pytest-examples
183184

184-
.PHONY: smoketest test coverage pytest-examples test-curl
185+
.PHONY: smoketest test coverage pytest-examples test-curl htmlcov
185186

186187
## --- Automated checks --------------------------------------------------------
187188
smoketest:
@@ -215,6 +216,18 @@ coverage:
215216
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage-badge -fo $(DOCS_DIR)/docs/images/coverage.svg"
216217
@echo "✅ Coverage artefacts: md, HTML in $(COVERAGE_DIR), XML & badge ✔"
217218

219+
htmlcov:
220+
@echo "📊 Generating HTML coverage report…"
221+
@test -d "$(VENV_DIR)" || $(MAKE) venv
222+
@mkdir -p $(COVERAGE_DIR)
223+
# If there's no existing coverage data, fall back to the full test-run
224+
@if [ ! -f .coverage ]; then \
225+
echo "ℹ️ No .coverage file found – running full coverage first…"; \
226+
$(MAKE) --no-print-directory coverage; \
227+
fi
228+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && coverage html -i -d $(COVERAGE_DIR)"
229+
@echo "✅ HTML coverage report ready → $(COVERAGE_DIR)/index.html"
230+
218231
pytest-examples:
219232
@echo "🧪 Testing README examples…"
220233
@test -d "$(VENV_DIR)" || $(MAKE) venv

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[![CodeQL](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml)&nbsp;
1010
[![Bandit Security](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml)&nbsp;
1111
[![Dependency Review](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml)&nbsp;
12+
[![Tests & Coverage](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml)&nbsp;
1213

1314
<!-- === Container Build & Deploy === -->
1415
[![Secure Docker Build](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml)&nbsp;

tests/unit/mcpgateway/test_cli.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# -*- coding: utf-8 -*-
2+
"""Tests for the mcpgateway CLI module (cli.py).
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Mihai Criveti
7+
8+
This module contains tests for the tiny "Uvicorn wrapper" found in
9+
mcpgateway.cli. It exercises **every** decision point:
10+
11+
* `_needs_app` - missing vs. present app path
12+
* `_insert_defaults` - all permutations of host/port injection
13+
* `main()` - early-return on --version / -V **and** the happy path that
14+
actually calls Uvicorn with a patched ``sys.argv``.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import importlib
20+
import sys
21+
from pathlib import Path
22+
from typing import List, Dict, Any
23+
24+
import pytest
25+
26+
import mcpgateway.cli as cli
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# helpers / fixtures
31+
# ---------------------------------------------------------------------------
32+
33+
34+
@pytest.fixture(autouse=True)
35+
def _restore_sys_argv() -> None:
36+
"""Keep the global *sys.argv* pristine between tests."""
37+
original = sys.argv.copy()
38+
yield
39+
sys.argv[:] = original
40+
41+
42+
def _capture_uvicorn_main(monkeypatch) -> Dict[str, Any]:
43+
"""Monkey-patch *uvicorn.main* and record the argv it sees."""
44+
captured: Dict[str, Any] = {}
45+
46+
def _fake_main() -> None:
47+
# Copy because tests mutate sys.argv afterwards.
48+
captured["argv"] = sys.argv.copy()
49+
50+
monkeypatch.setattr(cli.uvicorn, "main", _fake_main)
51+
return captured
52+
53+
54+
# ---------------------------------------------------------------------------
55+
# _needs_app
56+
# ---------------------------------------------------------------------------
57+
58+
59+
@pytest.mark.parametrize(
60+
("argv", "missing"),
61+
[
62+
([], True), # no positional args at all
63+
(["--reload"], True), # first token is an option
64+
(["somepkg.app:app"], False), # explicit app path present
65+
],
66+
)
67+
def test_needs_app_detection(argv: List[str], missing: bool) -> None:
68+
assert cli._needs_app(argv) is missing
69+
70+
71+
# ---------------------------------------------------------------------------
72+
# _insert_defaults
73+
# ---------------------------------------------------------------------------
74+
75+
76+
def test_insert_defaults_injects_everything() -> None:
77+
"""No app/host/port supplied ⇒ inject all three."""
78+
raw = ["--reload"]
79+
out = cli._insert_defaults(raw)
80+
81+
# original list must remain untouched (function copies)
82+
assert raw == ["--reload"]
83+
84+
assert out[0] == cli.DEFAULT_APP
85+
assert "--host" in out and cli.DEFAULT_HOST in out
86+
assert "--port" in out and str(cli.DEFAULT_PORT) in out
87+
88+
89+
def test_insert_defaults_respects_explicit_host(monkeypatch) -> None:
90+
"""Host given, port missing ⇒ only port default injected."""
91+
raw = ["myapp:app", "--host", "0.0.0.0"]
92+
out = cli._insert_defaults(raw)
93+
94+
# our app path must stay first
95+
assert out[0] == "myapp:app"
96+
# host left untouched, port injected
97+
assert out.count("--host") == 1
98+
assert "--port" in out and str(cli.DEFAULT_PORT) in out
99+
100+
101+
def test_insert_defaults_skips_for_uds() -> None:
102+
"""When --uds is present no host/port defaults are added."""
103+
raw = ["--uds", "/tmp/app.sock"]
104+
out = cli._insert_defaults(raw)
105+
106+
assert "--host" not in out
107+
assert "--port" not in out
108+
109+
110+
# ---------------------------------------------------------------------------
111+
# main() - early *--version* short-circuit
112+
# ---------------------------------------------------------------------------
113+
114+
115+
@pytest.mark.parametrize("flag", ["--version", "-V"])
116+
def test_main_prints_version_and_exits(flag: str, capsys, monkeypatch) -> None:
117+
monkeypatch.setattr(sys, "argv", ["mcpgateway", flag])
118+
# If Uvicorn accidentally ran we'd hang the tests - make sure it can't.
119+
monkeypatch.setattr(cli.uvicorn, "main", lambda: (_ for _ in ()).throw(RuntimeError("should not be called")))
120+
cli.main()
121+
122+
out, err = capsys.readouterr()
123+
assert out.strip() == f"mcpgateway {cli.__version__}"
124+
assert err == ""
125+
126+
127+
# ---------------------------------------------------------------------------
128+
# main() - normal execution path (calls Uvicorn)
129+
# ---------------------------------------------------------------------------
130+
131+
132+
def test_main_invokes_uvicorn_with_patched_argv(monkeypatch) -> None:
133+
"""Ensure *main()* rewrites argv then delegates to Uvicorn."""
134+
captured = _capture_uvicorn_main(monkeypatch)
135+
monkeypatch.setattr(sys, "argv", ["mcpgateway", "--reload"])
136+
137+
cli.main()
138+
139+
# The fake Uvicorn ran exactly once
140+
assert "argv" in captured
141+
patched = captured["argv"]
142+
143+
# Position 0 must be the console-script name
144+
assert patched[0] == "mcpgateway"
145+
# The injected app path must follow
146+
assert patched[1] == cli.DEFAULT_APP
147+
# Original flag preserved
148+
assert "--reload" in patched
149+
# Defaults present
150+
assert "--host" in patched and cli.DEFAULT_HOST in patched
151+
assert "--port" in patched and str(cli.DEFAULT_PORT) in patched

0 commit comments

Comments
 (0)