"""Tests for profiles.json version 2 (#103): environment → service → identity. Covers: v2 loading + flattening, dotted-path and alias resolution with strict order (exact alias → exact address → fail closed), legacy v1 names via aliases, fail-closed validation (missing/unknown version, malformed hierarchy, ambiguous selectors, TBD-* usernames, reviewer-identity deadlock rule, inline secrets, missing auth, unnormalizable operations), service-default inheritance, and that flattened v2 profiles still work with resolve_token. No network, no secrets. """ import os import sys import copy 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 FAKE_TOKEN = "fake-token-for-tests" # not a real credential def v2_config(): """A fresh, valid v2 config exercising both environments.""" return { "version": 2, "environments": { "prgs": { "services": { "gitea": { "base_url": "https://gitea.prgs.cc", "default_owner": "Scaled-Tech-Consulting", "identities": { "author": { "role": "author", "username": "jcwalker3", "auth": {"type": "keychain", "id": "prgs.gitea.author.token"}, "execution_profile": "prgs-author", "audit_label": "prgs-author", "allowed_operations": [ "gitea.read", "gitea.issue.create", "gitea.branch.push", "gitea.pr.create", ], "forbidden_operations": [ "gitea.pr.approve", "gitea.pr.merge", ], }, "reviewer": { "role": "reviewer", "username": "sysadmin", "auth": {"type": "env", "name": "PRGS_REVIEWER_TOKEN"}, "execution_profile": "prgs-reviewer", "audit_label": "prgs-reviewer", "default_repo": "Gitea-Tools", "allowed_operations": [ "read", "review", "comment", "approve", "request_changes", "merge", ], "forbidden_operations": [ "gitea.pr.create", "gitea.branch.push", ], }, }, }, }, }, "mdcps": { "services": { "gitea": { "base_url": "https://gitea.dadeschools.net", "identities": { "author": { "role": "author", "username": "913443", "auth": {"type": "keychain", "id": "mdcps.gitea.author.token"}, "allowed_operations": ["gitea.read"], "forbidden_operations": [ "gitea.pr.approve", "gitea.pr.merge", ], }, "reviewer": { "role": "reviewer", "username": "TBD-second-mdcps-user", "auth": {"type": "keychain", "id": "mdcps.gitea.reviewer.token"}, "allowed_operations": [ "gitea.read", "gitea.pr.approve", "gitea.pr.merge", ], "forbidden_operations": [ "gitea.pr.create", "gitea.branch.push", ], }, }, }, "jenkins": { "base_url": "https://jenkins.dadeschools.net", "identities": { "reader": { "role": "reader", "username": "svc-jenkins-read", "auth": {"type": "keychain", "id": "mdcps.jenkins.reader.token"}, "allowed_operations": ["read", "jenkins.build.read"], "forbidden_operations": ["jenkins.build.trigger"], }, }, }, }, }, }, "aliases": { "mdcps": "mdcps.gitea.author", "prgs-author": "prgs.gitea.author", "prgs-reviewer": "prgs.gitea.reviewer", }, } class _V2Base(unittest.TestCase): def setUp(self): self._dir = tempfile.TemporaryDirectory() self.path = os.path.join(self._dir.name, "profiles.json") self._write(v2_config()) def tearDown(self): self._dir.cleanup() def _write(self, obj): with open(self.path, "w", encoding="utf-8") as fh: fh.write(obj if isinstance(obj, str) else json.dumps(obj)) def _env(self, profile, **extra): env = {"GITEA_MCP_CONFIG": self.path, "GITEA_MCP_PROFILE": profile} env.update(extra) return env def _resolve(self, profile): with patch.dict(os.environ, self._env(profile), clear=True): return gitea_config.resolve_profile() def _load_raises(self, mutate, needle): cfg = v2_config() mutate(cfg) self._write(cfg) with patch.dict(os.environ, self._env("prgs.gitea.author"), clear=True): with self.assertRaises(gitea_config.ConfigError) as ctx: gitea_config.resolve_profile() self.assertIn(needle, str(ctx.exception)) return str(ctx.exception) # --------------------------------------------------------------------------- # Happy path: loading, dotted paths, aliases, inheritance # --------------------------------------------------------------------------- class TestV2Loads(_V2Base): def test_dotted_path_resolution(self): p = self._resolve("prgs.gitea.author") self.assertEqual(p["base_url"], "https://gitea.prgs.cc") self.assertEqual(p["username"], "jcwalker3") self.assertEqual(p["profile_path"], "prgs.gitea.author") self.assertEqual(p["environment"], "prgs") self.assertEqual(p["service"], "gitea") self.assertEqual(p["identity"], "author") self.assertEqual(p["role"], "author") def test_alias_resolution_legacy_names(self): for legacy, addr in ( ("mdcps", "mdcps.gitea.author"), ("prgs-author", "prgs.gitea.author"), ("prgs-reviewer", "prgs.gitea.reviewer"), ): p = self._resolve(legacy) self.assertEqual(p["profile_path"], addr, legacy) def test_service_defaults_inherit_and_identity_overrides(self): author = self._resolve("prgs.gitea.author") self.assertEqual(author["default_owner"], "Scaled-Tech-Consulting") self.assertNotIn("default_repo", author) reviewer = self._resolve("prgs.gitea.reviewer") self.assertEqual(reviewer["default_owner"], "Scaled-Tech-Consulting") self.assertEqual(reviewer["default_repo"], "Gitea-Tools") def test_unqualified_ops_normalized_minimally(self): reviewer = self._resolve("prgs.gitea.reviewer") self.assertIn("gitea.pr.merge", reviewer["allowed_operations"]) self.assertIn("gitea.read", reviewer["allowed_operations"]) self.assertNotIn("merge", reviewer["allowed_operations"]) jenkins = self._resolve("mdcps.jenkins.reader") self.assertIn("jenkins.read", jenkins["allowed_operations"]) self.assertIn("jenkins.build.read", jenkins["allowed_operations"]) def test_resolve_token_works_on_flattened_profile(self): with patch.dict( os.environ, self._env("prgs.gitea.reviewer", PRGS_REVIEWER_TOKEN=FAKE_TOKEN), clear=True, ): profile = gitea_config.resolve_profile() self.assertEqual(gitea_config.resolve_token(profile), FAKE_TOKEN) def test_auth_source_name_on_flattened_profile(self): p = self._resolve("mdcps.gitea.author") self.assertEqual( gitea_config.auth_source_name(p), "keychain:mdcps.gitea.author.token" ) def test_v1_config_still_loads(self): self._write({ "version": 1, "profiles": {"prgs": { "base_url": "https://gitea.prgs.cc", "auth": {"type": "keychain", "id": "prgs-gitea-token"}, }}, }) p = self._resolve("prgs") self.assertEqual(p["base_url"], "https://gitea.prgs.cc") def test_validate_config_accepts_valid_v2(self): self.assertEqual(gitea_config.validate_config(v2_config()), []) # --------------------------------------------------------------------------- # Fail-closed: selectors # --------------------------------------------------------------------------- class TestV2Selectors(_V2Base): def test_unknown_selector_fails_closed(self): with self.assertRaises(gitea_config.ConfigError) as ctx: self._resolve("prgs.gitea") # partial address — no fuzzy matching self.assertIn("not found", str(ctx.exception)) def test_no_fuzzy_matching_on_near_miss(self): with self.assertRaises(gitea_config.ConfigError): self._resolve("prgs-reviewers") def test_conflicting_alias_and_address_fails_closed(self): def mutate(cfg): cfg["aliases"]["prgs.gitea.author"] = "prgs.gitea.reviewer" self._load_raises(mutate, "conflicting selector") def test_alias_to_unknown_target_fails_closed(self): def mutate(cfg): cfg["aliases"]["ghost"] = "prgs.gitea.nope" self._load_raises(mutate, "unknown profile") def test_tbd_username_fails_closed_on_selection(self): with self.assertRaises(gitea_config.ConfigError) as ctx: self._resolve("mdcps.gitea.reviewer") msg = str(ctx.exception) self.assertIn("TBD", msg) self.assertIn("provision", msg) def test_tbd_identity_does_not_block_other_identities(self): # Same file contains the TBD reviewer; author still resolves. p = self._resolve("mdcps.gitea.author") self.assertEqual(p["username"], "913443") # --------------------------------------------------------------------------- # Fail-closed: structure and versions # --------------------------------------------------------------------------- class TestV2Structure(_V2Base): def test_missing_version_fails_closed(self): def mutate(cfg): del cfg["version"] self._load_raises(mutate, "version") def test_unknown_version_fails_closed(self): def mutate(cfg): cfg["version"] = 3 self._load_raises(mutate, "unsupported version") def test_missing_environments_fails_closed(self): def mutate(cfg): del cfg["environments"] self._load_raises(mutate, "environments") def test_malformed_environment_fails_closed(self): def mutate(cfg): cfg["environments"]["prgs"] = "not-an-object" self._load_raises(mutate, "must be a JSON object") def test_missing_services_fails_closed(self): def mutate(cfg): cfg["environments"]["prgs"]["services"] = {} self._load_raises(mutate, "services") def test_missing_identities_fails_closed(self): def mutate(cfg): cfg["environments"]["prgs"]["services"]["gitea"]["identities"] = {} self._load_raises(mutate, "identities") def test_dotted_segment_name_fails_closed(self): def mutate(cfg): envs = cfg["environments"] envs["bad.env"] = copy.deepcopy(envs["prgs"]) self._load_raises(mutate, "invalid environment name") def test_missing_base_url_fails_closed(self): def mutate(cfg): svc = cfg["environments"]["prgs"]["services"]["gitea"] del svc["base_url"] self._load_raises(mutate, "base_url") # --------------------------------------------------------------------------- # Fail-closed: identity invariants # --------------------------------------------------------------------------- class TestV2IdentityInvariants(_V2Base): def _ident(self, cfg, addr="prgs.gitea.author"): env, svc, ident = addr.split(".") return cfg["environments"][env]["services"][svc]["identities"][ident] def test_missing_auth_fails_closed(self): def mutate(cfg): del self._ident(cfg)["auth"] self._load_raises(mutate, "missing an 'auth' reference") def test_inline_secret_in_identity_rejected(self): def mutate(cfg): self._ident(cfg)["token"] = "oops-not-a-real-secret" msg = self._load_raises(mutate, "inline 'token'") self.assertNotIn("oops-not-a-real-secret", msg) def test_inline_secret_in_auth_rejected(self): def mutate(cfg): self._ident(cfg)["auth"]["password"] = "oops-not-a-real-secret" msg = self._load_raises(mutate, "inline 'password'") self.assertNotIn("oops-not-a-real-secret", msg) def test_reviewer_deadlock_invariant_enforced(self): def mutate(cfg): reviewer = self._ident(cfg, "prgs.gitea.reviewer") reviewer["forbidden_operations"] = [] # can approve/merge AND create msg = self._load_raises(mutate, "deadlock") self.assertIn("gitea.pr.create", msg) def test_reviewer_deadlock_applies_to_unqualified_merge(self): def mutate(cfg): author = self._ident(cfg) author["allowed_operations"] = ["merge"] # normalized to gitea.pr.merge author["forbidden_operations"] = [] self._load_raises(mutate, "deadlock") def test_unnormalizable_operation_fails_closed(self): def mutate(cfg): self._ident(cfg)["allowed_operations"] = ["frobnicate"] self._load_raises(mutate, "cannot be normalized") def test_foreign_namespace_operation_fails_closed(self): def mutate(cfg): reader = self._ident(cfg, "mdcps.jenkins.reader") reader["allowed_operations"] = ["gitea.pr.merge"] self._load_raises(mutate, "cannot be normalized") if __name__ == "__main__": unittest.main()