Files
Gitea-Tools/tests/test_create_issue.py
T
sysadmin dc899d23c8 fix(session): require config-backed mutation authority and workspace-bound targets (#714)
Close the #714 remediation gap left at 943d402 where env-only mutation
profiles, REMOTES Timesheet defaults treated as explicit caller input, and
machine-dependent Git remotes produced false greens.

- Fail closed when mutations lack a config-backed profile with non-empty
  allowed_repositories; env ops cannot invent mutation authority.
- Preserve omitted-vs-explicit org/repo provenance through the mutation
  gate; omitted targets bind to the verified workspace repository.
- Prefer workspace-aligned Git remotes over historical Timesheet defaults
  in MCP _resolve and CLI resolve_remote.
- Centralize deterministic config-backed mutation fixtures; migrate
  mutation tests off env-only authority without weakening security
  assertions.

Full suite: 2744 passed, 6 skipped.
2026-07-15 21:13:21 -04:00

218 lines
9.1 KiB
Python

import os
"""Tests for create_issue.py.
Every test mocks auth functions so no real network calls or keychain
access are performed.
Note: create_issue.py may be inaccessible due to macOS sandbox restrictions.
If so, these tests are automatically skipped.
"""
import io
import json
import sys
import tempfile
import unittest
import contextlib
from unittest.mock import patch, MagicMock, patch
# The module under test lives in the repo root, not a package.
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
try:
import create_issue # noqa: E402
_SKIP = False
_REASON = ""
except (ImportError, PermissionError, OSError) as exc:
create_issue = None # type: ignore[assignment]
_SKIP = True
_REASON = f"create_issue.py not importable: {exc}"
FAKE_CREDS = ("testuser", "testpass")
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestArgParsing(unittest.TestCase):
"""Ensure argparse accepts the expected flags and rejects bad input."""
@patch("create_issue.api_request", return_value={"number": 1, "html_url": "http://x/1"})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_minimal_args(self, _cred, _api):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = create_issue.main(["--title", "Hello"])
self.assertEqual(rc, 0)
def test_missing_title_exits(self):
with self.assertRaises(SystemExit) as ctx:
create_issue.main(["--body", "no title given"])
self.assertNotEqual(ctx.exception.code, 0)
@patch("create_issue.api_request", return_value={"number": 1, "html_url": "http://x/1"})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_remote_choices(self, _cred, _api):
for remote in ("dadeschools", "prgs"):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = create_issue.main(["--remote", remote, "--title", "X"])
self.assertEqual(rc, 0, f"--remote {remote} should be accepted")
def test_invalid_remote_exits(self):
with self.assertRaises(SystemExit):
create_issue.main(["--remote", "nonexistent", "--title", "X"])
# ---------------------------------------------------------------------------
# Remote resolution
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestRemoteResolution(unittest.TestCase):
"""Verify the correct host/org/repo are selected per remote."""
@patch("create_issue.api_request", return_value={"number": 1, "html_url": "http://x/1"})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_dadeschools_default(self, mock_cred, _api):
create_issue.main(["--title", "T"])
mock_cred.assert_called_with("gitea.dadeschools.net")
@patch("create_issue.api_request", return_value={"number": 1, "html_url": "http://x/1"})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_prgs_remote(self, mock_cred, _api):
create_issue.main(["--remote", "prgs", "--title", "T"])
mock_cred.assert_called_with("gitea.prgs.cc")
@patch("create_issue.api_request", return_value={"number": 1, "html_url": "http://x/1"})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_host_override(self, mock_cred, _api):
create_issue.main(["--host", "custom.example.com", "--title", "T"])
mock_cred.assert_called_with("custom.example.com")
# ---------------------------------------------------------------------------
# API payload
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestAPIPayload(unittest.TestCase):
"""#714: omitted --org/--repo uses workspace-bound repo (Gitea-Tools),
not the historical REMOTES Timesheet default. Intentional security-policy
migration of legacy omitted-target assertions.
"""
def setUp(self):
self._ws_env = patch.dict(
os.environ,
{
"GITEA_TEST_WORKSPACE_REMOTE_URL": (
"https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools.git"
)
},
clear=False,
)
self._ws_env.start()
def tearDown(self):
self._ws_env.stop()
"""Ensure the JSON payload sent to Gitea is correct."""
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_payload_title_and_body(self, _cred):
with patch("create_issue.api_request",
return_value={"number": 1, "html_url": "http://x/1"}) as mock_api:
create_issue.main(["--title", "My Title", "--body", "My Body"])
payload = mock_api.call_args[0][3]
self.assertEqual(payload["title"], "My Title")
self.assertEqual(payload["body"], "My Body")
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_applies_workflow_labels_by_name(self, _cred):
def api_side_effect(method, url, auth, payload=None):
if method == "GET" and url.endswith("/labels"):
return [
{"id": 1, "name": "type:feature"},
{"id": 2, "name": "status:ready"},
]
if method == "POST" and url.endswith("/issues"):
return {"number": 7, "html_url": "http://x/7"}
if method == "PUT" and url.endswith("/issues/7/labels"):
return [{"name": "type:feature"}, {"name": "status:ready"}]
return {}
with patch("create_issue.api_request", side_effect=api_side_effect) as mock_api:
rc = create_issue.main([
"--title", "My Title",
"--type-label", "feature",
"--status-label", "ready",
])
self.assertEqual(rc, 0)
put_call = next(c for c in mock_api.call_args_list if c[0][0] == "PUT")
self.assertEqual(put_call[0][3], {"labels": [1, 2]})
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_require_workflow_labels_blocks_missing_labels(self, _cred):
with patch("create_issue.api_request") as mock_api:
rc = create_issue.main(["--title", "My Title", "--require-workflow-labels"])
self.assertEqual(rc, 1)
mock_api.assert_not_called()
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_url_construction(self, _cred):
with patch("create_issue.api_request",
return_value={"number": 1, "html_url": "http://x/1"}) as mock_api:
create_issue.main(["--remote", "prgs", "--title", "X"])
url = mock_api.call_args[0][1]
self.assertEqual(
url,
"https://gitea.prgs.cc/api/v1/repos/Scaled-Tech-Consulting/Gitea-Tools/issues",
)
# ---------------------------------------------------------------------------
# Body file reading
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestBodyFile(unittest.TestCase):
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_body_from_file(self, _cred):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("File body content")
f.flush()
with patch("create_issue.api_request",
return_value={"number": 1, "html_url": "http://x/1"}) as mock_api:
create_issue.main(["--title", "T", "--body-file", f.name])
payload = mock_api.call_args[0][3]
self.assertEqual(payload["body"], "File body content")
# ---------------------------------------------------------------------------
# Auth failure
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestAuthFailure(unittest.TestCase):
@patch("create_issue.get_credentials", return_value=("", ""))
def test_no_credentials_returns_1(self, _cred):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = create_issue.main(["--title", "T"])
self.assertEqual(rc, 1)
# ---------------------------------------------------------------------------
# API error handling
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestAPIError(unittest.TestCase):
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_api_error_returns_1(self, _cred):
with patch("create_issue.api_request",
side_effect=RuntimeError("HTTP 422: duplicate")):
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
rc = create_issue.main(["--title", "Dup"])
self.assertEqual(rc, 1)
if __name__ == "__main__":
unittest.main()