Skip to content

Commit 777cd2e

Browse files
committed
Improve test coverage
Signed-off-by: Mihai Criveti <[email protected]>
1 parent fbb3445 commit 777cd2e

File tree

1 file changed

+128
-52
lines changed

1 file changed

+128
-52
lines changed
Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,167 @@
11
# -*- coding: utf-8 -*-
22
"""
3-
Unit tests for mcpgateway.utils.create_jwt_token
3+
Full-coverage unit tests for **mcpgateway.utils.create_jwt_token**
44
5-
Covered behaviour
6-
-----------------
7-
* _create_jwt_token round-trip (with exp claim present)
8-
* create_jwt_token async wrapper (with exp disabled)
9-
* get_jwt_token default helper
10-
* _decode_jwt_token convenience decoder
5+
All paths are exercised, including:
6+
* sync core (`_create_jwt_token`) with / without ``exp`` claim
7+
* async wrappers (`create_jwt_token`, `get_jwt_token`)
8+
* helper `_decode_jwt_token`
9+
* CLI helpers: `_payload_from_cli`, `_parse_args`, and `main()` in both
10+
encode (`--pretty`) and decode (`--decode`) modes.
1111
12-
No CLI tests here—the CLI path is just thin plumbing around the same core
13-
functions and would add subprocess complexity for little gain.
12+
No subprocesses – we invoke `main()` directly, patching ``sys.argv`` and
13+
capturing stdout with ``capsys``.
14+
15+
Running:
16+
17+
pytest -q --cov=mcpgateway.utils.create_jwt_token --cov-report=term-missing
18+
19+
should show **100 %** statement coverage for the target module.
1420
1521
Copyright 2025
1622
SPDX-License-Identifier: Apache-2.0
1723
Author: Your Name
1824
"""
19-
import asyncio
25+
from __future__ import annotations
26+
27+
import json
28+
import sys
29+
from types import SimpleNamespace
30+
from typing import Any, Dict
2031

2132
import jwt
2233
import pytest
2334

24-
# --------------------------------------------------------------------------- #
25-
# Import the module under test #
26-
# --------------------------------------------------------------------------- #
2735
from mcpgateway.utils import create_jwt_token as jwt_util # noqa: E402
2836

29-
# Simple aliases to keep the tests tidy
30-
_create_jwt_token = jwt_util._create_jwt_token # pylint: disable=protected-access
31-
create_jwt_token = jwt_util.create_jwt_token
32-
get_jwt_token = jwt_util.get_jwt_token
33-
_decode_jwt_token = jwt_util._decode_jwt_token # pylint: disable=protected-access
34-
3537
# --------------------------------------------------------------------------- #
36-
# Helpers #
38+
# Patch module-level constants **before** we start calling helpers #
3739
# --------------------------------------------------------------------------- #
3840
TEST_SECRET = "unit-test-secret"
3941
TEST_ALGO = "HS256"
4042

43+
jwt_util.DEFAULT_SECRET = TEST_SECRET
44+
jwt_util.DEFAULT_ALGO = TEST_ALGO
45+
# NB: settings.jwt_secret_key is read at *runtime* in _decode(), so patch too
46+
jwt_util.settings.jwt_secret_key = TEST_SECRET
47+
jwt_util.settings.jwt_algorithm = TEST_ALGO
48+
49+
# Short aliases keep test lines tidy
50+
_create: Any = jwt_util._create_jwt_token # pylint: disable=protected-access
51+
_decode: Any = jwt_util._decode_jwt_token # pylint: disable=protected-access
52+
_payload: Any = jwt_util._payload_from_cli # pylint: disable=protected-access
53+
_parse_args: Any = jwt_util._parse_args # pylint: disable=protected-access
54+
create_async = jwt_util.create_jwt_token
55+
get_default = jwt_util.get_jwt_token
56+
main_cli = jwt_util.main
57+
4158

4259
# --------------------------------------------------------------------------- #
43-
# Tests #
60+
# Helpers #
4461
# --------------------------------------------------------------------------- #
45-
def test_sync_token_roundtrip_with_exp():
46-
"""_create_jwt_token ➜ jwt.decode should reproduce original payload (plus exp)."""
47-
payload = {"foo": "bar"}
62+
def _ns(**kw) -> SimpleNamespace:
63+
"""Namespace helper for _payload_from_cli tests."""
64+
defaults = {"username": None, "data": None}
65+
defaults.update(kw)
66+
return SimpleNamespace(**defaults)
4867

49-
token = _create_jwt_token(
50-
payload,
51-
expires_in_minutes=1,
52-
secret=TEST_SECRET,
53-
algorithm=TEST_ALGO,
54-
)
5568

56-
decoded = jwt.decode(token, TEST_SECRET, algorithms=[TEST_ALGO])
69+
# --------------------------------------------------------------------------- #
70+
# Core token helpers #
71+
# --------------------------------------------------------------------------- #
72+
def test_create_token_paths():
73+
"""_create_jwt_token with and without exp claim."""
74+
payload: Dict[str, Any] = {"foo": "bar"}
5775

58-
# Original data retained
59-
for k, v in payload.items():
60-
assert decoded[k] == v
76+
tok1 = _create(payload, expires_in_minutes=1, secret=TEST_SECRET, algorithm=TEST_ALGO)
77+
dec1 = jwt.decode(tok1, TEST_SECRET, algorithms=[TEST_ALGO])
78+
assert dec1["foo"] == "bar" and "exp" in dec1
6179

62-
# exp claim present and is int
63-
assert isinstance(decoded["exp"], int)
80+
tok2 = _create(payload, expires_in_minutes=0, secret=TEST_SECRET, algorithm=TEST_ALGO)
81+
assert jwt.decode(tok2, TEST_SECRET, algorithms=[TEST_ALGO]) == payload
6482

6583

6684
@pytest.mark.asyncio
67-
async def test_async_wrapper_without_exp():
68-
"""create_jwt_token async wrapper works and omits exp when minutes==0."""
69-
payload = {"a": 1}
85+
async def test_async_wrappers():
86+
"""create_jwt_token & get_jwt_token wrappers work end-to-end."""
7087

71-
token = await create_jwt_token(
72-
payload,
73-
expires_in_minutes=0, # disable exp claim
88+
# Explicit secret/algorithm keep this token verifiable with _decode()
89+
token = await create_async(
90+
{"k": "v"},
91+
expires_in_minutes=0,
7492
secret=TEST_SECRET,
7593
algorithm=TEST_ALGO,
7694
)
95+
assert _decode(token) == {"k": "v"}
7796

78-
decoded = jwt.decode(token, TEST_SECRET, algorithms=[TEST_ALGO])
79-
assert decoded == payload # no extra keys
97+
# get_jwt_token uses the original secret captured at definition time;
98+
# just decode without verifying the signature to inspect the payload.
99+
admin_token = await get_default()
100+
payload = jwt.decode(admin_token, options={"verify_signature": False})
101+
assert payload["username"] == jwt_util.DEFAULT_USERNAME
80102

81103

82-
@pytest.mark.asyncio
83-
async def test_get_default_admin_token(monkeypatch):
84-
"""get_jwt_token should emit a token containing DEFAULT_USERNAME."""
85-
# The helper relies on module-level DEFAULT_* constants initialised at import
86-
# time; we therefore *decode* with whatever secret the module already holds.
87-
token = await get_jwt_token()
104+
# --------------------------------------------------------------------------- #
105+
# _payload_from_cli variants #
106+
# --------------------------------------------------------------------------- #
107+
def test_payload_username():
108+
assert _payload(_ns(username="alice")) == {"username": "alice"}
109+
110+
111+
def test_payload_json():
112+
assert _payload(_ns(data='{"a": 1}')) == {"a": 1}
113+
114+
115+
def test_payload_keyvals():
116+
assert _payload(_ns(data="x=1, y=two")) == {"x": "1", "y": "two"}
88117

89-
decoded = _decode_jwt_token(token)
90118

91-
assert decoded["username"] == jwt_util.DEFAULT_USERNAME
119+
def test_payload_invalid_pair():
120+
with pytest.raises(ValueError):
121+
_payload(_ns(data="oops"))
122+
123+
124+
def test_payload_default():
125+
assert _payload(_ns()) == {"username": jwt_util.DEFAULT_USERNAME}
126+
127+
128+
# --------------------------------------------------------------------------- #
129+
# CLI arg-parsing & main() #
130+
# --------------------------------------------------------------------------- #
131+
def test_parse_args():
132+
sys.argv = ["prog", "-u", "bob", "-e", "10"]
133+
args = _parse_args()
134+
assert args.username == "bob" and args.exp == 10 and args.data is None
135+
136+
137+
def test_main_encode_pretty(capsys):
138+
"""main() in encode mode prints payload then token."""
139+
sys.argv = [
140+
"prog",
141+
"-u",
142+
"cliuser",
143+
"-e",
144+
"0",
145+
"-s",
146+
TEST_SECRET,
147+
"--algo",
148+
TEST_ALGO,
149+
"--pretty",
150+
]
151+
main_cli()
152+
153+
out_lines = capsys.readouterr().out.strip().splitlines()
154+
assert out_lines[0] == "Payload:"
155+
token = out_lines[-1]
156+
assert jwt.decode(token, TEST_SECRET, algorithms=[TEST_ALGO])["username"] == "cliuser"
157+
158+
159+
def test_main_decode_mode(capsys):
160+
"""main() in decode mode prints JSON payload."""
161+
token = _create({"z": 9}, 0, TEST_SECRET, TEST_ALGO)
162+
sys.argv = ["prog", "--decode", token, "--algo", TEST_ALGO]
163+
164+
main_cli()
165+
166+
printed = capsys.readouterr().out.strip()
167+
assert json.loads(printed) == {"z": 9}

0 commit comments

Comments
 (0)