|
1 | 1 | # -*- coding: utf-8 -*-
|
2 | 2 | """
|
3 |
| -Unit tests for mcpgateway.utils.create_jwt_token |
| 3 | +Full-coverage unit tests for **mcpgateway.utils.create_jwt_token** |
4 | 4 |
|
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. |
11 | 11 |
|
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. |
14 | 20 |
|
15 | 21 | Copyright 2025
|
16 | 22 | SPDX-License-Identifier: Apache-2.0
|
17 | 23 | Author: Your Name
|
18 | 24 | """
|
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 |
20 | 31 |
|
21 | 32 | import jwt
|
22 | 33 | import pytest
|
23 | 34 |
|
24 |
| -# --------------------------------------------------------------------------- # |
25 |
| -# Import the module under test # |
26 |
| -# --------------------------------------------------------------------------- # |
27 | 35 | from mcpgateway.utils import create_jwt_token as jwt_util # noqa: E402
|
28 | 36 |
|
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 |
| - |
35 | 37 | # --------------------------------------------------------------------------- #
|
36 |
| -# Helpers # |
| 38 | +# Patch module-level constants **before** we start calling helpers # |
37 | 39 | # --------------------------------------------------------------------------- #
|
38 | 40 | TEST_SECRET = "unit-test-secret"
|
39 | 41 | TEST_ALGO = "HS256"
|
40 | 42 |
|
| 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 | + |
41 | 58 |
|
42 | 59 | # --------------------------------------------------------------------------- #
|
43 |
| -# Tests # |
| 60 | +# Helpers # |
44 | 61 | # --------------------------------------------------------------------------- #
|
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) |
48 | 67 |
|
49 |
| - token = _create_jwt_token( |
50 |
| - payload, |
51 |
| - expires_in_minutes=1, |
52 |
| - secret=TEST_SECRET, |
53 |
| - algorithm=TEST_ALGO, |
54 |
| - ) |
55 | 68 |
|
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"} |
57 | 75 |
|
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 |
61 | 79 |
|
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 |
64 | 82 |
|
65 | 83 |
|
66 | 84 | @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.""" |
70 | 87 |
|
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, |
74 | 92 | secret=TEST_SECRET,
|
75 | 93 | algorithm=TEST_ALGO,
|
76 | 94 | )
|
| 95 | + assert _decode(token) == {"k": "v"} |
77 | 96 |
|
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 |
80 | 102 |
|
81 | 103 |
|
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"} |
88 | 117 |
|
89 |
| - decoded = _decode_jwt_token(token) |
90 | 118 |
|
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