"""Tests for JSON runtime-profile configuration (gitea_config) and its integration into gitea_auth.get_profile / get_auth_header. Covers: env-only still works, JSON selection, multiple profiles, env-override precedence, missing file, invalid JSON, missing/unset profile, inline-token rejection + redaction, and that config parsing performs no network calls. """ import os import sys import json import tempfile import unittest from unittest.mock import patch sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent)) import gitea_config # noqa: E402 import gitea_auth # noqa: E402 CONFIG = { "profiles": { "dev": { "base_url": "https://gitea.dev.example", "profile_name": "gitea-author", "token_env": "GITEA_TOKEN_DEV", "owner": "Org", "repo": "Repo", "allowed_operations": ["read", "pr.create"], "forbidden_operations": ["merge"], "audit_label": "dev-author", }, "prod-readonly": { "base_url": "https://gitea.prod.example", "profile_name": "gitea-readonly", "token_env": "GITEA_TOKEN_PROD", "allowed_operations": ["read"], "audit_label": "prod-ro", }, } } class _ConfigBase(unittest.TestCase): def setUp(self): self._dir = tempfile.TemporaryDirectory() self.path = os.path.join(self._dir.name, "gitea-mcp.json") self._write(CONFIG) def tearDown(self): self._dir.cleanup() def _write(self, obj): with open(self.path, "w", encoding="utf-8") as fh: if isinstance(obj, str): fh.write(obj) else: json.dump(obj, fh) def _env(self, profile="dev", **extra): env = {"GITEA_MCP_CONFIG": self.path, "GITEA_MCP_PROFILE": profile} env.update(extra) return env # --------------------------------------------------------------------------- # gitea_config: loading / selection # --------------------------------------------------------------------------- class TestLoadSelect(_ConfigBase): def test_env_only_returns_none(self): # No GITEA_MCP_CONFIG -> JSON layer off, no error. with patch.dict(os.environ, {}, clear=True): self.assertIsNone(gitea_config.load_config()) self.assertIsNone(gitea_config.resolve_profile()) def test_selected_profile_loads(self): with patch.dict(os.environ, self._env("dev"), clear=True): p = gitea_config.resolve_profile() self.assertEqual(p["profile_name"], "gitea-author") self.assertEqual(p["base_url"], "https://gitea.dev.example") def test_multiple_profiles_select_correctly(self): with patch.dict(os.environ, self._env("prod-readonly"), clear=True): p = gitea_config.resolve_profile() self.assertEqual(p["profile_name"], "gitea-readonly") self.assertEqual(p["allowed_operations"], ["read"]) def test_missing_file_when_unset_is_noop(self): with patch.dict(os.environ, {}, clear=True): self.assertIsNone(gitea_config.resolve_profile()) def test_missing_file_when_set_raises(self): env = {"GITEA_MCP_CONFIG": self.path + ".nope", "GITEA_MCP_PROFILE": "dev"} with patch.dict(os.environ, env, clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() self.assertIn("missing file", str(ctx.exception)) def test_invalid_json_raises_without_leaking_content(self): self._write('{"profiles": {"dev": {"token": "super-secret-token" BROKEN}}}') with patch.dict(os.environ, self._env("dev"), clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() msg = str(ctx.exception) self.assertIn("invalid JSON", msg) self.assertNotIn("super-secret-token", msg) # no file content leaked def test_missing_profile_raises_with_available(self): with patch.dict(os.environ, self._env("ghost"), clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() msg = str(ctx.exception) self.assertIn("ghost", msg) self.assertIn("dev", msg) # lists available profiles def test_config_set_but_profile_unset_raises(self): with patch.dict(os.environ, {"GITEA_MCP_CONFIG": self.path}, clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() self.assertIn("GITEA_MCP_PROFILE", str(ctx.exception)) def test_bad_top_level_shape_raises(self): self._write({"nope": 1}) with patch.dict(os.environ, self._env("dev"), clear=True): with self.assertRaises(gitea_config.ConfigError): gitea_config.resolve_profile() # --------------------------------------------------------------------------- # Token handling / redaction # --------------------------------------------------------------------------- class TestTokenHandling(_ConfigBase): def test_inline_token_rejected_without_echo(self): self._write({"profiles": {"dev": {"token": "super-secret-token"}}}) with patch.dict(os.environ, self._env("dev"), clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() msg = str(ctx.exception) self.assertIn("token_env", msg) self.assertNotIn("super-secret-token", msg) # value never echoed def test_resolve_token_reads_env_by_name(self): env = self._env("dev", GITEA_TOKEN_DEV="dev-token-value") with patch.dict(os.environ, env, clear=True): p = gitea_config.resolve_profile() self.assertEqual(gitea_config.resolve_token(p), "dev-token-value") def test_resolve_token_none_when_env_unset(self): with patch.dict(os.environ, self._env("dev"), clear=True): p = gitea_config.resolve_profile() self.assertIsNone(gitea_config.resolve_token(p)) def test_resolve_token_none_for_none_profile(self): self.assertIsNone(gitea_config.resolve_token(None)) def test_profile_metadata_never_contains_token_value(self): env = self._env("dev", GITEA_TOKEN_DEV="dev-token-value") with patch.dict(os.environ, env, clear=True): p = gitea_config.resolve_profile() self.assertNotIn("dev-token-value", json.dumps(p)) # --------------------------------------------------------------------------- # gitea_auth integration: get_profile precedence + get_auth_header fallback # --------------------------------------------------------------------------- class TestAuthIntegration(_ConfigBase): def test_env_only_profile_unchanged(self): # No JSON config: get_profile is exactly the env-only behaviour. with patch.dict(os.environ, {"GITEA_PROFILE_NAME": "gitea-reviewer", "GITEA_ALLOWED_OPERATIONS": "read,review"}, clear=True): p = gitea_auth.get_profile() self.assertEqual(p["profile_name"], "gitea-reviewer") self.assertEqual(p["allowed_operations"], ["read", "review"]) self.assertIsNone(p["base_url"]) def test_json_fills_profile_when_env_absent(self): with patch.dict(os.environ, self._env("dev"), clear=True): p = gitea_auth.get_profile() self.assertEqual(p["profile_name"], "gitea-author") self.assertEqual(p["allowed_operations"], ["read", "pr.create"]) self.assertEqual(p["forbidden_operations"], ["merge"]) self.assertEqual(p["audit_label"], "dev-author") self.assertEqual(p["base_url"], "https://gitea.dev.example") # token_env surfaces as the (non-secret) token *source name*. self.assertEqual(p["token_source_name"], "GITEA_TOKEN_DEV") def test_env_overrides_json(self): env = self._env("dev", GITEA_PROFILE_NAME="env-name", GITEA_ALLOWED_OPERATIONS="read", GITEA_BASE_URL="https://env.example") with patch.dict(os.environ, env, clear=True): p = gitea_auth.get_profile() self.assertEqual(p["profile_name"], "env-name") self.assertEqual(p["allowed_operations"], ["read"]) self.assertEqual(p["base_url"], "https://env.example") def test_get_profile_propagates_config_error(self): with patch.dict(os.environ, self._env("ghost"), clear=True): with self.assertRaises(gitea_config.ConfigError): gitea_auth.get_profile() def test_auth_header_uses_json_token_env(self): env = self._env("dev", GITEA_TOKEN_DEV="json-token") with patch.dict(os.environ, env, clear=True): header = gitea_auth.get_auth_header("gitea.example.com") self.assertEqual(header, "token json-token") def test_explicit_env_token_overrides_json(self): env = self._env("dev", GITEA_TOKEN_DEV="json-token", GITEA_TOKEN="env-token") with patch.dict(os.environ, env, clear=True): header = gitea_auth.get_auth_header("gitea.example.com") self.assertEqual(header, "token env-token") def test_auth_header_config_error_fails_closed(self): # A broken selection must not crash auth — it falls back to no token. env = {"GITEA_MCP_CONFIG": self.path, "GITEA_MCP_PROFILE": "ghost"} with patch.dict(os.environ, env, clear=True): with patch("gitea_auth.get_credentials", return_value=("", "")): self.assertIsNone(gitea_auth.get_auth_header("gitea.example.com")) # --------------------------------------------------------------------------- # No network during config parsing # --------------------------------------------------------------------------- class TestNoNetwork(_ConfigBase): def test_config_load_makes_no_network_calls(self): boom = AssertionError("config parsing must not touch the network") with patch.dict(os.environ, self._env("dev"), clear=True): with patch("gitea_auth.urllib.request.urlopen", side_effect=boom): self.assertIsNotNone(gitea_config.resolve_profile()) # get_profile only reads config + env; no HTTP. self.assertEqual( gitea_auth.get_profile()["profile_name"], "gitea-author") if __name__ == "__main__": unittest.main()