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:
@@ -0,0 +1,141 @@
|
||||
"""Tests for manage_labels.py.
|
||||
|
||||
All API calls are mocked — no real network or keychain access.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
||||
import manage_labels # noqa: E402
|
||||
|
||||
|
||||
FAKE_AUTH = "Basic dGVzdDp0ZXN0" # base64("test:test")
|
||||
|
||||
|
||||
def _make_label(name, lid):
|
||||
return {"id": lid, "name": name, "color": "000000"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Label creation
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestLabelCreation(unittest.TestCase):
|
||||
"""Verify create-or-skip logic for the label set."""
|
||||
|
||||
@patch("manage_labels.get_auth_header", return_value=FAKE_AUTH)
|
||||
@patch("manage_labels.api")
|
||||
def test_skips_existing_labels(self, mock_api, _auth):
|
||||
# Simulate all labels already exist
|
||||
existing = [_make_label(l["name"], i) for i, l in enumerate(manage_labels.LABELS)]
|
||||
mock_api.return_value = existing # first call is GET /labels
|
||||
|
||||
# Patch sys.argv to avoid --dry
|
||||
with patch.object(sys, "argv", ["manage_labels.py"]):
|
||||
manage_labels.main()
|
||||
|
||||
# The GET call happens, but no POST calls for label creation
|
||||
get_calls = [c for c in mock_api.call_args_list if c[0][0] == "GET"]
|
||||
post_label_calls = [
|
||||
c for c in mock_api.call_args_list
|
||||
if c[0][0] == "POST" and c[0][1] == "/labels"
|
||||
]
|
||||
self.assertGreaterEqual(len(get_calls), 1)
|
||||
self.assertEqual(len(post_label_calls), 0)
|
||||
|
||||
@patch("manage_labels.get_auth_header", return_value=FAKE_AUTH)
|
||||
@patch("manage_labels.api")
|
||||
def test_creates_missing_labels(self, mock_api, _auth):
|
||||
# Simulate no existing labels
|
||||
def side_effect(method, path, auth, payload=None):
|
||||
if method == "GET" and "/labels" in path:
|
||||
return [] # no existing labels
|
||||
if method == "POST" and path == "/labels":
|
||||
return {"id": 999, "name": payload["name"]}
|
||||
if method == "PUT":
|
||||
return [{"name": n} for n in (payload or {}).get("labels", [])]
|
||||
return None
|
||||
|
||||
mock_api.side_effect = side_effect
|
||||
with patch.object(sys, "argv", ["manage_labels.py"]):
|
||||
manage_labels.main()
|
||||
|
||||
post_calls = [
|
||||
c for c in mock_api.call_args_list
|
||||
if c[0][0] == "POST" and c[0][1] == "/labels"
|
||||
]
|
||||
self.assertEqual(len(post_calls), len(manage_labels.LABELS))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dry run mode
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestDryRun(unittest.TestCase):
|
||||
|
||||
@patch("manage_labels.get_auth_header", return_value=FAKE_AUTH)
|
||||
@patch("manage_labels.api")
|
||||
def test_dry_run_makes_no_writes(self, mock_api, _auth):
|
||||
mock_api.return_value = [] # no existing labels
|
||||
|
||||
with patch.object(sys, "argv", ["manage_labels.py", "--dry"]):
|
||||
manage_labels.main()
|
||||
|
||||
# Only the GET call should be made, no POST or PUT
|
||||
for c in mock_api.call_args_list:
|
||||
method = c[0][0]
|
||||
self.assertEqual(method, "GET",
|
||||
f"Dry run should not call {method}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Label mapping
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestLabelMapping(unittest.TestCase):
|
||||
|
||||
@patch("manage_labels.get_auth_header", return_value=FAKE_AUTH)
|
||||
@patch("manage_labels.api")
|
||||
def test_applies_mapping_to_issues(self, mock_api, _auth):
|
||||
existing = [_make_label(l["name"], i + 1) for i, l in enumerate(manage_labels.LABELS)]
|
||||
|
||||
def side_effect(method, path, auth, payload=None):
|
||||
if method == "GET":
|
||||
return existing
|
||||
if method == "PUT":
|
||||
return [{"name": "applied"}]
|
||||
return None
|
||||
|
||||
mock_api.side_effect = side_effect
|
||||
with patch.object(sys, "argv", ["manage_labels.py"]):
|
||||
manage_labels.main()
|
||||
|
||||
put_calls = [c for c in mock_api.call_args_list if c[0][0] == "PUT"]
|
||||
self.assertEqual(len(put_calls), len(manage_labels.MAPPING))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LABELS and MAPPING constants
|
||||
# ---------------------------------------------------------------------------
|
||||
class TestConstants(unittest.TestCase):
|
||||
"""Sanity-check the hardcoded label set and mapping."""
|
||||
|
||||
def test_status_in_progress_label_exists(self):
|
||||
names = [l["name"] for l in manage_labels.LABELS]
|
||||
self.assertIn("status:in-progress", names)
|
||||
|
||||
def test_all_mapped_labels_are_defined(self):
|
||||
defined = {l["name"] for l in manage_labels.LABELS}
|
||||
for issue, names in manage_labels.MAPPING.items():
|
||||
for name in names:
|
||||
self.assertIn(name, defined,
|
||||
f"Issue #{issue} maps to undefined label '{name}'")
|
||||
|
||||
def test_label_colors_are_valid_hex(self):
|
||||
import re
|
||||
for label in manage_labels.LABELS:
|
||||
self.assertRegex(label["color"], r"^[0-9a-fA-F]{6}$",
|
||||
f"Label '{label['name']}' has invalid color")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user