"""Regression coverage for the remote/repo mismatch guard (#530). Bare ``remote=prgs`` historically resolved to ``Scaled-Tech-Consulting/Timesheet`` (the hardcoded ``REMOTES`` default) instead of ``Scaled-Tech-Consulting/Gitea-Tools``, which is what the local git remote actually points at. That silent mismatch caused false 404s and risked mutating the wrong repository. The guard fails closed when the MCP-resolved org/repo disagrees with the local git remote URL and the caller did not pass explicit ``org``/``repo``. """ import os import unittest from unittest import mock import remote_repo_guard LOCAL_GITEA_TOOLS_URL = "https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools.git" class TestAssessRemoteRepoMatch(unittest.TestCase): def test_explicit_org_and_repo_skips_guard(self): # Caller supplied both org and repo explicitly: never block, even on mismatch. assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Timesheet", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=True, repo_explicit=True, ) self.assertTrue(assessment["proven"]) self.assertFalse(assessment["block"]) self.assertEqual(assessment["reasons"], []) def test_missing_local_remote_url_skips_guard(self): # Best-effort lookup: no local remote URL (non-checkout usage) => do not block. assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Timesheet", local_remote_url=None, org_explicit=False, repo_explicit=False, ) self.assertTrue(assessment["proven"]) self.assertFalse(assessment["block"]) def test_matching_repo_is_proven(self): assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Gitea-Tools", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=False, repo_explicit=False, ) self.assertTrue(assessment["proven"]) self.assertFalse(assessment["block"]) def test_regression_prgs_default_timesheet_vs_local_gitea_tools(self): # The exact #530 scenario: default repo Timesheet, local remote Gitea-Tools. assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Timesheet", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=False, repo_explicit=False, ) self.assertFalse(assessment["proven"]) self.assertTrue(assessment["block"]) self.assertTrue(assessment["reasons"]) self.assertEqual(assessment["resolved_repo"], "Timesheet") self.assertEqual(assessment["resolved_org"], "Scaled-Tech-Consulting") def test_only_org_explicit_still_checks_repo(self): # Repo left as default => still guarded even if org was explicit. assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Timesheet", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=True, repo_explicit=False, ) self.assertTrue(assessment["block"]) def test_case_insensitive_match(self): assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="scaled-tech-consulting", resolved_repo="gitea-tools", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=False, repo_explicit=False, ) self.assertTrue(assessment["proven"]) def test_format_error_mentions_resolved_and_local(self): assessment = remote_repo_guard.assess_remote_repo_match( remote="prgs", resolved_org="Scaled-Tech-Consulting", resolved_repo="Timesheet", local_remote_url=LOCAL_GITEA_TOOLS_URL, org_explicit=False, repo_explicit=False, ) message = remote_repo_guard.format_remote_repo_guard_error(assessment) self.assertIn("Timesheet", message) self.assertIn("Gitea-Tools", message) self.assertIn("org=", message) self.assertIn("repo=", message) class TestResolveServerWiring(unittest.TestCase): """The guard is wired into gitea_mcp_server._resolve, so every read/lookup/ mutation tool that resolves a target fails closed on a repo mismatch. Under pytest the guard is bypassed unless GITEA_FORCE_REMOTE_REPO_CHECK is set, so these tests set the force flag and patch the local remote lookup + REMOTES. """ def setUp(self): import gitea_mcp_server self.server = gitea_mcp_server force = mock.patch.dict( os.environ, {"GITEA_FORCE_REMOTE_REPO_CHECK": "1"}, clear=False ) force.start() self.addCleanup(force.stop) url_patch = mock.patch.object( gitea_mcp_server, "_local_git_remote_url", return_value=LOCAL_GITEA_TOOLS_URL, ) url_patch.start() self.addCleanup(url_patch.stop) def _set_prgs_default_repo(self, repo): original = dict(self.server.REMOTES["prgs"]) self.addCleanup(self.server.REMOTES.__setitem__, "prgs", original) self.server.REMOTES["prgs"] = {**original, "repo": repo} def test_resolve_blocks_on_default_repo_mismatch(self): self._set_prgs_default_repo("Timesheet") with self.assertRaises(RuntimeError) as ctx: self.server._resolve("prgs", None, None, None) self.assertIn("Timesheet", str(ctx.exception)) self.assertIn("Gitea-Tools", str(ctx.exception)) def test_resolve_allows_explicit_org_and_repo(self): self._set_prgs_default_repo("Timesheet") # Explicit org/repo is authoritative even if it does not match local remote. host, org, repo = self.server._resolve( "prgs", None, "Scaled-Tech-Consulting", "Timesheet" ) self.assertEqual((org, repo), ("Scaled-Tech-Consulting", "Timesheet")) def test_resolve_allows_matching_default(self): self._set_prgs_default_repo("Gitea-Tools") host, org, repo = self.server._resolve("prgs", None, None, None) self.assertEqual((org, repo), ("Scaled-Tech-Consulting", "Gitea-Tools")) def test_lookup_tool_cannot_silently_query_different_repo(self): # gitea_view_issue resolves the target via _resolve first, so a bare # remote=prgs pointing at the wrong default repo fails closed before any # API call is made. self._set_prgs_default_repo("Timesheet") with self.assertRaises(RuntimeError): self.server.gitea_view_issue(issue_number=1, remote="prgs") if __name__ == "__main__": unittest.main()