Skip to content

Commit 625c8dd

Browse files
committed
test_version.py tests
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent fe7d88f commit 625c8dd

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

tests/unit/mcpgateway/test_version.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# -*- coding: utf-8 -*-
2+
"""test_version.py - full-coverage unit tests for diagnostics endpoint
3+
4+
Copyright 2025
5+
SPDX-License-Identifier: Apache-2.0
6+
Authors: Mihai Criveti
7+
8+
This suite drives every code path in :pyfile:`mcpgateway/version.py`.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import re
14+
import types
15+
from typing import Any, Dict
16+
17+
import pytest
18+
from fastapi import FastAPI, HTTPException
19+
from fastapi.testclient import TestClient
20+
21+
22+
# --------------------------------------------------------------------------- #
23+
# Utility - fake psutil so _system_metrics code path runs #
24+
# --------------------------------------------------------------------------- #
25+
def _make_fake_psutil() -> types.ModuleType: # noqa: D401
26+
"""Return an in-memory *psutil* stub implementing just what we need."""
27+
class _MemInfo:
28+
def __init__(self, total: int, used: int) -> None:
29+
self.total, self.used = total, used
30+
31+
class _CPUFreq:
32+
current = 2_400
33+
34+
class _ProcMem: # noqa: D401 - simple struct
35+
rss, vms = 10 * 1_048_576, 20 * 1_048_576
36+
37+
class _Proc:
38+
pid = 1234
39+
40+
def num_fds(self) -> int: # noqa: D401
41+
return 8
42+
43+
def cpu_percent(self, interval: float = 0.0) -> float: # noqa: D401
44+
return 1.5
45+
46+
def memory_info(self) -> _ProcMem: # noqa: D401
47+
return _ProcMem()
48+
49+
def num_threads(self) -> int: # noqa: D401
50+
return 5
51+
52+
def _disk_usage(path: str): # noqa: D401 - simple namespace
53+
return types.SimpleNamespace(
54+
total=100 * 1_073_741_824,
55+
used=40 * 1_073_741_824,
56+
)
57+
58+
fake = types.ModuleType("fake_psutil")
59+
fake.virtual_memory = lambda: _MemInfo(8 * 1_073_741_824, 4 * 1_073_741_824)
60+
fake.swap_memory = lambda: _MemInfo(2 * 1_073_741_824, 1 * 1_073_741_824)
61+
fake.cpu_freq = lambda: _CPUFreq()
62+
fake.cpu_percent = lambda interval=0.0: 12.3
63+
fake.cpu_count = lambda logical=True: 8
64+
fake.boot_time = lambda: 0
65+
fake.Process = _Proc
66+
fake.disk_usage = _disk_usage
67+
return fake
68+
69+
70+
# --------------------------------------------------------------------------- #
71+
# Helper - build test app #
72+
# --------------------------------------------------------------------------- #
73+
def _build_app(monkeypatch: pytest.MonkeyPatch, auth_ok: bool = True) -> FastAPI:
74+
"""Return an isolated FastAPI app with only the diagnostics router."""
75+
from mcpgateway import version as ver_mod
76+
77+
# Stub heavy helpers
78+
monkeypatch.setattr(ver_mod, "_database_version", lambda: ("db-vX", True))
79+
monkeypatch.setattr(ver_mod, "_system_metrics", lambda: {"stub": True})
80+
monkeypatch.setattr(ver_mod, "_git_revision", lambda: "deadbeef")
81+
monkeypatch.setattr(ver_mod, "REDIS_AVAILABLE", False, raising=False)
82+
83+
# Auth override
84+
async def _allow() -> Dict[str, str]:
85+
return {"user": "tester"}
86+
87+
async def _deny() -> None:
88+
raise HTTPException(status_code=401)
89+
90+
app = FastAPI()
91+
app.include_router(ver_mod.router)
92+
app.dependency_overrides[ver_mod.require_auth] = _allow if auth_ok else _deny
93+
return app
94+
95+
96+
@pytest.fixture()
97+
def client(monkeypatch: pytest.MonkeyPatch) -> TestClient:
98+
"""Authenticated *TestClient* fixture."""
99+
return TestClient(_build_app(monkeypatch, auth_ok=True))
100+
101+
102+
# --------------------------------------------------------------------------- #
103+
# Endpoint - happy path #
104+
# --------------------------------------------------------------------------- #
105+
def test_version_json_ok(client: TestClient) -> None:
106+
rsp = client.get("/version")
107+
assert rsp.status_code == 200
108+
assert rsp.headers["content-type"].startswith("application/json")
109+
payload: Dict[str, Any] = rsp.json()
110+
assert payload["database"]["server_version"] == "db-vX"
111+
assert payload["system"] == {"stub": True}
112+
assert payload["app"]["git_revision"] == "deadbeef"
113+
114+
115+
def test_version_html_query_param(client: TestClient) -> None:
116+
rsp = client.get("/version?fmt=html")
117+
assert rsp.status_code == 200
118+
assert rsp.headers["content-type"].startswith("text/html")
119+
assert "<!doctype html>" in rsp.text.lower()
120+
121+
122+
def test_version_html_accept_header(client: TestClient) -> None:
123+
rsp = client.get("/version", headers={"accept": "text/html"})
124+
assert rsp.status_code == 200
125+
assert rsp.headers["content-type"].startswith("text/html")
126+
assert "<h1" in rsp.text
127+
128+
129+
def test_version_html_all_sections(client: TestClient) -> None:
130+
html = client.get("/version?fmt=html").text
131+
for sec in ["App", "Platform", "Database", "Redis", "Settings", "System", "Environment"]:
132+
assert re.search(rf"<h2[^>]*>{sec}</h2>", html)
133+
134+
135+
# --------------------------------------------------------------------------- #
136+
# Authentication #
137+
# --------------------------------------------------------------------------- #
138+
def test_version_requires_auth(monkeypatch: pytest.MonkeyPatch) -> None:
139+
unauth_client = TestClient(_build_app(monkeypatch, auth_ok=False))
140+
rsp = unauth_client.get("/version")
141+
assert rsp.status_code == 401
142+
143+
144+
# --------------------------------------------------------------------------- #
145+
# Helper functions #
146+
# --------------------------------------------------------------------------- #
147+
def test_is_secret_and_public_env(monkeypatch: pytest.MonkeyPatch) -> None:
148+
from mcpgateway import version as ver_mod
149+
150+
monkeypatch.setenv("PLAIN", "1")
151+
monkeypatch.setenv("X_SECRET", "bad")
152+
assert "PLAIN" in ver_mod._public_env()
153+
assert "X_SECRET" not in ver_mod._public_env()
154+
155+
156+
def test_sanitize_url() -> None:
157+
from mcpgateway import version as ver_mod
158+
159+
url = "postgres://u:p@host:5432/db"
160+
assert ver_mod._sanitize_url(url) == "postgres://u@host:5432/db"
161+
162+
163+
def test_git_revision_env(monkeypatch: pytest.MonkeyPatch) -> None:
164+
from mcpgateway import version as ver_mod
165+
166+
monkeypatch.setenv("GIT_COMMIT", "0123456789abcdef")
167+
assert ver_mod._git_revision() == "012345678"
168+
169+
170+
def test_git_revision_subprocess(monkeypatch: pytest.MonkeyPatch) -> None:
171+
"""Ensure subprocess branch returns short hash."""
172+
from mcpgateway import version as ver_mod
173+
174+
# Remove env override
175+
monkeypatch.delenv("GIT_COMMIT", raising=False)
176+
177+
fake_subprocess = types.SimpleNamespace(
178+
check_output=lambda *a, **kw: b"abc123\n",
179+
DEVNULL=-3, # needed by _git_revision
180+
)
181+
monkeypatch.setattr(ver_mod, "subprocess", fake_subprocess)
182+
assert ver_mod._git_revision() == "abc123"
183+
184+
185+
# --------------------------------------------------------------------------- #
186+
# _database_version branches #
187+
# --------------------------------------------------------------------------- #
188+
def test_database_version_success(monkeypatch: pytest.MonkeyPatch) -> None:
189+
from mcpgateway import version as ver_mod
190+
191+
class _Conn:
192+
def __enter__(self): # noqa: D401
193+
return self
194+
195+
def __exit__(self, *exc): # noqa: D401
196+
pass
197+
198+
def execute(self, stmt): # noqa: D401
199+
class _Res:
200+
scalar = lambda self: "15.0" # noqa: D401
201+
return _Res()
202+
203+
class _Engine:
204+
dialect = types.SimpleNamespace(name="postgresql")
205+
206+
def connect(self): # noqa: D401
207+
return _Conn()
208+
209+
monkeypatch.setattr(ver_mod, "engine", _Engine())
210+
ver, ok = ver_mod._database_version()
211+
assert ok and ver == "15.0"
212+
213+
214+
def test_database_version_error(monkeypatch: pytest.MonkeyPatch) -> None:
215+
from mcpgateway import version as ver_mod
216+
217+
class _BrokenEngine:
218+
dialect = types.SimpleNamespace(name="sqlite")
219+
220+
def connect(self): # noqa: D401
221+
raise RuntimeError("boom")
222+
223+
monkeypatch.setattr(ver_mod, "engine", _BrokenEngine())
224+
ver, ok = ver_mod._database_version()
225+
assert not ok and "boom" in ver
226+
227+
228+
# --------------------------------------------------------------------------- #
229+
# _system_metrics with fake psutil #
230+
# --------------------------------------------------------------------------- #
231+
def test_system_metrics_full(monkeypatch: pytest.MonkeyPatch) -> None:
232+
from mcpgateway import version as ver_mod
233+
234+
monkeypatch.setattr(ver_mod, "psutil", _make_fake_psutil())
235+
metrics = ver_mod._system_metrics()
236+
assert metrics["process"]["pid"] == 1234

0 commit comments

Comments
 (0)