test: add comprehensive test suite

- test_create_issue.py: arg parsing, remote resolution, payload, body-file, auth, HTTP errors
  (auto-skips if create_issue.py is inaccessible due to macOS sandbox)
- test_create_pr.py: arg parsing, remote resolution, payload fields, default base, auth, HTTP errors
- test_credentials.py: get_credentials() parsing, password with '=', empty output, stdin verification
- test_manage_labels.py: label creation (skip/create), dry run, mapping application, constant validation
- test_shell_scripts.py: close_issue.sh and mark_issue.sh arg validation and error messages

28 passed, 12 skipped (macOS sandbox on create_issue.py).
This commit is contained in:
2026-06-21 17:26:18 -04:00
parent 7404f768d3
commit c4c9993039
6 changed files with 620 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
"""Tests for create_issue.py.
Every test mocks `get_credentials` and `urllib.request.urlopen` 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
from unittest.mock import 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")
def _mock_urlopen(status=200, body=None):
"""Return a mock context-manager for urllib.request.urlopen."""
if body is None:
body = {"number": 42, "html_url": "https://gitea.example.com/issues/42"}
resp = MagicMock()
resp.read.return_value = json.dumps(body).encode("utf-8")
resp.__enter__ = lambda s: s
resp.__exit__ = MagicMock(return_value=False)
return resp
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestArgParsing(unittest.TestCase):
"""Ensure argparse accepts the expected flags and rejects bad input."""
@patch("create_issue.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_minimal_args(self, _cred, _url):
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.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_remote_choices(self, _cred, _url):
for remote in ("dadeschools", "prgs"):
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.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_dadeschools_default(self, mock_cred, mock_url):
create_issue.main(["--title", "T"])
mock_cred.assert_called_with("gitea.dadeschools.net")
@patch("create_issue.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_prgs_remote(self, mock_cred, mock_url):
create_issue.main(["--remote", "prgs", "--title", "T"])
mock_cred.assert_called_with("gitea.prgs.cc")
@patch("create_issue.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_host_override(self, mock_cred, mock_url):
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):
"""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.urllib.request.Request") as MockReq:
MockReq.return_value = MagicMock()
with patch("create_issue.urllib.request.urlopen",
return_value=_mock_urlopen()):
create_issue.main(["--title", "My Title", "--body", "My Body"])
# Inspect the data kwarg passed to Request(url, data=..., ...)
call_kwargs = MockReq.call_args
data = json.loads(call_kwargs[1]["data"].decode("utf-8"))
self.assertEqual(data["title"], "My Title")
self.assertEqual(data["body"], "My Body")
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_url_construction(self, _cred):
with patch("create_issue.urllib.request.Request") as MockReq:
MockReq.return_value = MagicMock()
with patch("create_issue.urllib.request.urlopen",
return_value=_mock_urlopen()):
create_issue.main(["--remote", "prgs", "--title", "X"])
url = MockReq.call_args[0][0]
self.assertEqual(
url,
"https://gitea.prgs.cc/api/v1/repos/Scaled-Tech-Consulting/Timesheet/issues",
)
# ---------------------------------------------------------------------------
# Body file reading
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestBodyFile(unittest.TestCase):
@patch("create_issue.urllib.request.urlopen", return_value=_mock_urlopen())
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_body_from_file(self, _cred, _url):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("File body content")
f.flush()
with patch("create_issue.urllib.request.Request") as MockReq:
MockReq.return_value = MagicMock()
with patch("create_issue.urllib.request.urlopen",
return_value=_mock_urlopen()):
create_issue.main(["--title", "T", "--body-file", f.name])
data = json.loads(MockReq.call_args[1]["data"].decode("utf-8"))
self.assertEqual(data["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):
rc = create_issue.main(["--title", "T"])
self.assertEqual(rc, 1)
# ---------------------------------------------------------------------------
# HTTP error handling
# ---------------------------------------------------------------------------
@unittest.skipIf(_SKIP, _REASON)
class TestHTTPError(unittest.TestCase):
@patch("create_issue.get_credentials", return_value=FAKE_CREDS)
def test_http_error_returns_1(self, _cred):
import urllib.error
err = urllib.error.HTTPError(
url="https://example.com", code=422, msg="Unprocessable",
hdrs=None, fp=io.BytesIO(b'{"message":"duplicate"}'),
)
with patch("create_issue.urllib.request.urlopen", side_effect=err):
rc = create_issue.main(["--title", "Dup"])
self.assertEqual(rc, 1)
if __name__ == "__main__":
unittest.main()