Files
rune/tests/test_gitea_provision.py
T
Nick ca4f7593d7
CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 14s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 34s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 1m4s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 48s
feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
CORRECTION 1: #314 reopened — recall_with_associations mode field defect
CONFIRMED in current source and FIXED (mode: association_merged).

CORRECTION 3: Gitea credential removed from remote URL (credential helper).
TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires
Gitea web UI access). Remote URL now shows username only, no secret.

CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence
computed warranted bump from commits-since-tag without checking if master
version already satisfies the bump. FIX: already-converged guard in
version_convergence.py (master version > last tag → no commit). Repo-aware
(args.repo passed to read_version/latest_tag).

CORRECTION 6: lifecycle terminalization — execution exception now
guarantees task terminalizes as failed with evidence (no immortal RUNNING).

CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction
(NOT shared operator state). Read-only leading-verb override. Isolation
tests prove no cross-contamination between interleaved tasks.

PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure
class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics,
route-instruction learning loop (PE-1789102658-0269 SEED).

1253 tests green; ratchet 352/80 (shrink); henxels clean.
2026-09-11 15:36:08 +01:00

808 lines
32 KiB
Python

import base64
import contextlib
import http.client
import http.server
import io
import json
import os
import subprocess
import sys
import tempfile
import threading
import unittest
import urllib.parse
import urllib.request
from copy import deepcopy
from pathlib import Path
from unittest import mock
from shared.gitea_provision import (
ManifestError,
ProvisioningError,
Provisioner,
UrllibTransport,
_SameOriginRedirectHandler,
load_manifest,
main,
normalize_color,
normalize_instant,
run,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
TRACKED_MANIFEST = REPOSITORY_ROOT / "config/gitea_repository_state.json"
class _RedirectServer:
def __init__(self, handler):
self.server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
@property
def url(self):
host, port = self.server.server_address
return f"http://{host}:{port}"
def __enter__(self):
self.thread.start()
return self
def __exit__(self, *_args):
self.server.shutdown()
self.thread.join()
self.server.server_close()
class UrllibTransportTests(unittest.TestCase):
def test_cli_reports_malformed_base_url_without_traceback_or_input_leakage(self):
unsafe_url = "https://[::1/unsafe-base-url-marker"
stderr = io.StringIO()
argv = [
"gitea_provision.py",
"--manifest",
str(TRACKED_MANIFEST),
"--gitea-url",
unsafe_url,
"--check",
]
with (
mock.patch("sys.argv", argv),
mock.patch(
"shared.gitea_provision._default_token_loader",
return_value="test-token",
),
mock.patch("urllib.request.build_opener") as build_opener,
contextlib.redirect_stderr(stderr),
):
result = main()
output = stderr.getvalue()
self.assertEqual(result, 2)
self.assertEqual(output, "error: Gitea API base URL is invalid\n")
self.assertNotIn("Traceback", output)
self.assertNotIn(unsafe_url, output)
self.assertNotIn("unsafe-base-url-marker", output)
build_opener.assert_not_called()
def test_plaintext_non_loopback_base_url_is_rejected_before_request(self):
token = "plaintext-transport-test-secret"
with mock.patch("urllib.request.build_opener") as build_opener:
with self.assertRaises(ProvisioningError) as raised:
UrllibTransport("http://example.com", token).request("GET", "/api/v1/version")
self.assertIn("HTTPS", str(raised.exception))
self.assertNotIn(token, str(raised.exception))
build_opener.assert_not_called()
def test_plaintext_literal_loopback_base_urls_are_allowed(self):
for base_url in (
"http://localhost:3000",
"http://127.42.0.9:3000",
"http://[::1]:3000",
):
with self.subTest(base_url=base_url):
transport = UrllibTransport(base_url, "loopback-test-secret")
self.assertEqual(transport.base_url, base_url)
def test_http_error_omits_server_controlled_content_and_retains_request_context(
self,
):
token = "runtime-token-boundary-test-secret"
response_body = f"server diagnostic containing {token}"
class TokenError(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(500, f"server reason containing {token}")
self.end_headers()
self.wfile.write(response_body.encode())
def log_message(self, *_args):
pass
with _RedirectServer(TokenError) as server:
transport = UrllibTransport(server.url, token)
with self.assertRaises(ProvisioningError) as raised:
transport.request("GET", "/token-error")
message = str(raised.exception)
self.assertEqual(message, "Gitea API GET /token-error failed (500)")
self.assertNotIn("server reason", message)
self.assertNotIn("server diagnostic", message)
self.assertNotIn(token, message)
def test_cross_origin_redirect_is_rejected_without_forwarding_authorization(self):
received_authorization = []
response = mock.Mock()
request = urllib.request.Request("http://127.0.0.1/source")
headers = http.client.HTTPMessage()
headers["Location"] = "http://127.0.0.2/destination"
with self.assertRaisesRegex(ProvisioningError, "cross-origin redirect"):
_SameOriginRedirectHandler().http_error_302(
request,
response,
302,
"Found",
headers,
)
response.close.assert_called_once_with()
class Destination(http.server.BaseHTTPRequestHandler):
def do_GET(self):
received_authorization.append(self.headers.get("Authorization"))
self.send_response(200)
self.end_headers()
self.wfile.write(b"{}")
def log_message(self, *_args):
pass
with _RedirectServer(Destination) as destination:
target = f"{destination.url}/captured"
class Redirect(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", target)
self.end_headers()
def log_message(self, *_args):
pass
with _RedirectServer(Redirect) as source:
transport = UrllibTransport(source.url, "transport-test-secret")
with self.assertRaisesRegex(ProvisioningError, "cross-origin redirect"):
transport.request("GET", "/redirect")
self.assertEqual(received_authorization, [])
def test_same_origin_redirect_preserves_authorization(self):
received_authorization = []
class Redirect(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/redirect":
self.send_response(302)
self.send_header("Location", "/destination")
self.end_headers()
return
received_authorization.append(self.headers.get("Authorization"))
self.send_response(200)
self.end_headers()
self.wfile.write(b"{}")
def log_message(self, *_args):
pass
with _RedirectServer(Redirect) as server:
result = UrllibTransport(server.url, "same-origin-test-secret").request("GET", "/redirect")
self.assertEqual(result, {})
self.assertEqual(received_authorization, ["token same-origin-test-secret"])
def test_malformed_redirect_location_raises_sanitized_provisioning_error(self):
token = "malformed-redirect-test-secret"
location = f"http://127.0.0.1:not-a-port/server-location-marker/{token}"
response = mock.Mock()
request = urllib.request.Request("http://127.0.0.1/redirect")
headers = http.client.HTTPMessage()
headers["Location"] = location
with self.assertRaisesRegex(ProvisioningError, "malformed redirect"):
_SameOriginRedirectHandler().http_error_302(
request,
response,
302,
"Found",
headers,
)
response.close.assert_called_once_with()
class MalformedRedirect(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", location)
self.end_headers()
def log_message(self, *_args):
pass
with _RedirectServer(MalformedRedirect) as server:
transport = UrllibTransport(server.url, token)
with self.assertRaises(ProvisioningError) as raised:
transport.request("GET", "/redirect")
message = str(raised.exception)
self.assertEqual(message, "Gitea API refused malformed redirect")
self.assertNotIn(location, message)
self.assertNotIn("server-location-marker", message)
self.assertNotIn(token, message)
def test_malformed_ipv6_redirect_location_raises_sanitized_provisioning_error(self):
token = "malformed-ipv6-redirect-test-secret"
location = "https://[::1/unsafe-redirect-marker"
class MalformedRedirect(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", location)
self.end_headers()
def log_message(self, *_args):
pass
with _RedirectServer(MalformedRedirect) as server:
transport = UrllibTransport(server.url, token)
with self.assertRaises(ProvisioningError) as raised:
transport.request("GET", "/redirect")
message = str(raised.exception)
self.assertEqual(message, "Gitea API refused malformed redirect")
self.assertNotIn(location, message)
self.assertNotIn("unsafe-redirect-marker", message)
self.assertNotIn(token, message)
def test_successful_response_with_invalid_json_raises_sanitized_error(self):
token = "invalid-json-test-secret"
class InvalidJson(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(f'{{"secret":"{token}"'.encode())
def log_message(self, *_args):
pass
with _RedirectServer(InvalidJson) as server:
transport = UrllibTransport(server.url, token)
with self.assertRaises(ProvisioningError) as raised:
transport.request("GET", "/invalid-json")
message = str(raised.exception)
self.assertIn("invalid JSON response", message)
self.assertNotIn(token, message)
self.assertNotIn("secret", message)
def test_cli_reports_invalid_json_without_traceback_or_token_leakage(self):
token = "cli-invalid-json-test-secret"
class InvalidJson(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(f'{{"secret":"{token}"'.encode())
def log_message(self, *_args):
pass
with _RedirectServer(InvalidJson) as server:
stderr = io.StringIO()
argv = [
"gitea_provision.py",
"--manifest",
str(TRACKED_MANIFEST),
"--gitea-url",
server.url,
"--check",
]
with (
mock.patch("sys.argv", argv),
mock.patch("shared.gitea_provision._default_token_loader", return_value=token),
contextlib.redirect_stderr(stderr),
):
result = main()
output = stderr.getvalue()
self.assertEqual(result, 2)
self.assertIn("invalid JSON response", output)
self.assertNotIn("Traceback", output)
self.assertNotIn(token, output)
self.assertNotIn("secret", output)
def test_cli_reports_response_read_errors_without_traceback_or_body_content(self):
token = "cli-read-timeout-test-secret"
response_content = f"response body containing {token}"
argv = [
"gitea_provision.py",
"--manifest",
str(TRACKED_MANIFEST),
"--check",
]
for error_type in (TimeoutError, OSError):
with self.subTest(error_type=error_type.__name__):
opener = mock.MagicMock()
opener.open.return_value.__enter__.return_value.read.side_effect = error_type(response_content)
stderr = io.StringIO()
with (
mock.patch("sys.argv", argv),
mock.patch(
"shared.gitea_provision._default_token_loader",
return_value=token,
),
mock.patch("urllib.request.build_opener", return_value=opener),
contextlib.redirect_stderr(stderr),
):
result = main()
output = stderr.getvalue()
self.assertEqual(result, 2)
self.assertIn("failed while reading response", output)
self.assertNotIn("Traceback", output)
self.assertNotIn(response_content, output)
self.assertNotIn(token, output)
def test_cli_reports_incomplete_response_read_without_exception_content(self):
token = "cli-incomplete-read-test-secret"
partial_response = f"partial response containing {token}".encode()
opener = mock.MagicMock()
opener.open.return_value.__enter__.return_value.read.side_effect = http.client.IncompleteRead(
partial_response,
len(partial_response) + 10,
)
stderr = io.StringIO()
argv = [
"gitea_provision.py",
"--manifest",
str(TRACKED_MANIFEST),
"--check",
]
with (
mock.patch("sys.argv", argv),
mock.patch("shared.gitea_provision._default_token_loader", return_value=token),
mock.patch("urllib.request.build_opener", return_value=opener),
contextlib.redirect_stderr(stderr),
):
result = main()
output = stderr.getvalue()
self.assertEqual(result, 2)
self.assertIn("failed while reading response", output)
self.assertNotIn("IncompleteRead", output)
self.assertNotIn("Traceback", output)
self.assertNotIn("partial response", output)
self.assertNotIn(token, output)
def test_cli_http_error_omits_response_body_containing_only_runtime_token_prefix(
self,
):
token = "cli-http-prefix-test-secret"
token_prefix = token[:15]
class PrefixError(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(500)
self.end_headers()
self.wfile.write(token_prefix.encode())
def log_message(self, *_args):
pass
with _RedirectServer(PrefixError) as server:
stderr = io.StringIO()
argv = [
"gitea_provision.py",
"--manifest",
str(TRACKED_MANIFEST),
"--gitea-url",
server.url,
"--check",
]
with (
mock.patch("sys.argv", argv),
mock.patch("shared.gitea_provision._default_token_loader", return_value=token),
contextlib.redirect_stderr(stderr),
):
result = main()
output = stderr.getvalue()
self.assertEqual(result, 2)
self.assertIn("Gitea API GET /api/v1/version failed (500)", output)
self.assertNotIn("Internal Server Error", output)
self.assertNotIn(token_prefix, output)
class FixturePathTests(unittest.TestCase):
def test_config_and_wiki_fixture_test_passes_from_external_cwd(self):
with tempfile.TemporaryDirectory() as external_cwd:
environment = os.environ.copy()
environment["PYTHONPATH"] = str(REPOSITORY_ROOT)
result = subprocess.run(
[
sys.executable,
"-m",
"unittest",
"tests.test_gitea_provision.ProvisionerTests.test_tracked_manifest_requires_all_managed_object_counts",
],
cwd=external_cwd,
env=environment,
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
DESIRED = {
"repository": {"owner": "conductor", "name": "rune", "default_branch": "master"},
"branch_protection": {
"branch_name": "master",
"enable_push": True,
"enable_push_whitelist": True,
"push_whitelist_usernames": [],
"enable_force_push": False,
"enable_merge_whitelist": False,
"required_approvals": 0,
"enable_status_check": True,
"status_check_contexts": ["CI Pipeline / required-checks (pull_request)"],
"block_on_rejected_reviews": True,
"block_on_outdated_branch": True,
"dismiss_stale_approvals": True,
},
"labels": [
{"name": "priority/high", "color": "#D73A4A", "description": "High priority"},
{"name": "type/bug", "color": "0075CA", "description": "Something is broken"},
],
"milestones": [
{
"title": "Foundation",
"description": "Build the base",
"state": "open",
"due_on": "2026-08-31T00:00:00Z",
}
],
"wiki_pages": [
{"title": "Home", "slug": "Home", "source": "docs/wiki/Home.md"},
],
}
class FakeTransport:
WIKI_PAGES_PATH = "/api/v1/repos/conductor/rune/wiki/pages"
WIKI_NEW_PATH = "/api/v1/repos/conductor/rune/wiki/new"
def __init__(self, state=None, page_size=50):
state = state or {}
self.branch = deepcopy(state.get("branch"))
self.labels = deepcopy(state.get("labels", []))
self.milestones = deepcopy(state.get("milestones", []))
self.wiki = deepcopy(state.get("wiki", {}))
self.calls = []
self.page_size = page_size
self.next_id = 100
def request(self, method, path, *, query=None, payload=None):
self.calls.append((method, path, deepcopy(query), deepcopy(payload)))
if path.endswith("/branch_protections"):
if method == "GET":
return [] if self.branch is None else [deepcopy(self.branch)]
self.branch = deepcopy(payload)
return deepcopy(self.branch)
if "/branch_protections/" in path:
self.branch.update(deepcopy(payload))
return deepcopy(self.branch)
if path.endswith("/labels"):
if method == "GET":
return self._page(self.labels, query)
item = {"id": self._id(), **deepcopy(payload)}
self.labels.append(item)
return deepcopy(item)
if "/labels/" in path:
item = self._by_id(self.labels, path)
item.update(deepcopy(payload))
return deepcopy(item)
if path.endswith("/milestones"):
if method == "GET":
self.assert_state_all(query)
return self._page(self.milestones, query)
item = {"id": self._id(), **deepcopy(payload)}
self.milestones.append(item)
return deepcopy(item)
if "/milestones/" in path:
item = self._by_id(self.milestones, path)
item.update(deepcopy(payload))
return deepcopy(item)
if path == self.WIKI_PAGES_PATH and method == "GET":
pages = [{"title": value["title"], "sub_url": key} for key, value in self.wiki.items()]
return self._page(pages, query)
if path == self.WIKI_NEW_PATH and method == "POST":
decoded = base64.b64decode(payload["content_base64"]).decode("utf-8")
self.wiki[payload["title"]] = {
"title": payload["title"],
"content": decoded,
}
return deepcopy(self.wiki[payload["title"]])
if "/wiki/page/" in path:
slug = urllib.parse.unquote(path.rsplit("/", 1)[1])
if method == "GET":
return deepcopy(self.wiki[slug])
decoded = base64.b64decode(payload["content_base64"]).decode("utf-8")
self.wiki[slug] = {"title": payload["title"], "content": decoded}
return deepcopy(self.wiki[slug])
raise AssertionError(f"unexpected request: {method} {path}")
def _page(self, values, query):
page = int((query or {}).get("page", 1))
limit = int((query or {}).get("limit", self.page_size))
start = (page - 1) * limit
return deepcopy(values[start : start + limit])
def _id(self):
self.next_id += 1
return self.next_id
@staticmethod
def _by_id(values, path):
wanted = int(path.rsplit("/", 1)[1])
return next(value for value in values if value["id"] == wanted)
@staticmethod
def assert_state_all(query):
if (query or {}).get("state") != "all":
raise AssertionError("milestone reads must request state=all")
class ProvisionerTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
(self.root / "docs/wiki").mkdir(parents=True)
(self.root / "docs/wiki/Home.md").write_text("# Rune\n\nCanonical wiki.\n", encoding="utf-8")
self.manifest = deepcopy(DESIRED)
def tearDown(self):
self.temp.cleanup()
def provisioner(self, transport):
return Provisioner(self.manifest, transport, repository_root=self.root, page_size=2)
def converged_state(self):
branch = deepcopy(self.manifest["branch_protection"])
branch["rule_name"] = branch.pop("branch_name")
labels = [{"id": index + 1, **value} for index, value in enumerate(self.manifest["labels"])]
milestones = [{"id": 20, **self.manifest["milestones"][0]}]
wiki = {"Home": {"title": "Home", "content": "# Rune\n\nCanonical wiki.\n"}}
return {
"branch": branch,
"labels": labels,
"milestones": milestones,
"wiki": wiki,
}
def test_empty_state_reports_every_managed_object_and_check_is_get_only(self):
transport = FakeTransport()
drift = self.provisioner(transport).check()
self.assertEqual(
{item.kind for item in drift},
{"branch protection", "label", "milestone", "wiki page"},
)
self.assertTrue(all(call[0] == "GET" for call in transport.calls))
def test_apply_creates_empty_state_and_verifies_convergence(self):
transport = FakeTransport()
result = self.provisioner(transport).apply()
self.assertEqual(result, [])
self.assertIn("Home", transport.wiki)
wiki_write = next(
call for call in transport.calls if call[0] == "POST" and call[1] == "/api/v1/repos/conductor/rune/wiki/new"
)
self.assertEqual(
base64.b64decode(wiki_write[3]["content_base64"]).decode(),
"# Rune\n\nCanonical wiki.\n",
)
def test_apply_updates_drift_but_preserves_unmanaged_objects(self):
state = self.converged_state()
state["branch"]["enable_force_push"] = True
state["labels"][0]["color"] = "ffffff"
state["labels"].append({"id": 99, "name": "manual", "color": "abcdef", "description": "keep"})
state["milestones"][0]["description"] = "stale"
state["milestones"].append({"id": 98, "title": "Unmanaged", "description": "keep", "state": "closed"})
state["wiki"]["Home"]["content"] = "stale"
state["wiki"]["Notes"] = {"title": "Notes", "content": "keep"}
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).apply(), [])
self.assertIn("manual", {label["name"] for label in transport.labels})
self.assertIn("Unmanaged", {milestone["title"] for milestone in transport.milestones})
self.assertIn("Notes", transport.wiki)
def test_converged_check_and_second_apply_have_no_writes(self):
transport = FakeTransport(self.converged_state())
self.assertEqual(self.provisioner(transport).check(), [])
transport.calls.clear()
self.assertEqual(self.provisioner(transport).apply(), [])
self.assertTrue(all(call[0] == "GET" for call in transport.calls))
def test_wiki_base64_response_is_decoded_and_does_not_trigger_a_write(self):
state = self.converged_state()
state["branch"]["enable_force_push"] = True
content = state["wiki"]["Home"].pop("content")
state["wiki"]["Home"]["content_base64"] = base64.b64encode(content.encode()).decode()
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).apply(), [])
wiki_writes = [call for call in transport.calls if call[0] != "GET" and "/wiki/" in call[1]]
self.assertEqual(wiki_writes, [])
def test_wiki_slug_is_url_encoded_as_one_path_segment(self):
source = self.root / "docs/wiki/Ops FAQ.md"
source.write_text("# Ops FAQ\n", encoding="utf-8")
self.manifest["wiki_pages"] = [
{"title": "Ops FAQ", "slug": "Ops / FAQ", "source": "docs/wiki/Ops FAQ.md"},
]
state = self.converged_state()
state["wiki"] = {"Ops / FAQ": {"title": "Ops FAQ", "content": "# Ops FAQ\n"}}
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).check(), [])
self.assertIn(
(
"GET",
"/api/v1/repos/conductor/rune/wiki/page/Ops%20%2F%20FAQ",
None,
None,
),
transport.calls,
)
def test_paginated_list_endpoints_read_later_pages(self):
state = self.converged_state()
state["labels"] = [
{"id": 90, "name": "manual-a", "color": "aaaaaa", "description": ""},
{"id": 91, "name": "manual-b", "color": "bbbbbb", "description": ""},
*state["labels"],
]
state["milestones"] = [
{"id": 92, "title": "Old A", "description": "", "state": "closed"},
{"id": 93, "title": "Old B", "description": "", "state": "closed"},
*state["milestones"],
]
state["wiki"] = {
"Notes-A": {"title": "Notes A", "content": "a"},
"Notes-B": {"title": "Notes B", "content": "b"},
**state["wiki"],
}
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).check(), [])
paged_paths = [call[1] for call in transport.calls if call[2] and call[2].get("page", 1) > 1]
self.assertTrue(any(path.endswith("/labels") for path in paged_paths))
self.assertTrue(any(path.endswith("/milestones") for path in paged_paths))
self.assertTrue(any(path.endswith("/wiki/pages") for path in paged_paths))
def test_branch_protections_reads_fifty_items_once_without_pagination(self):
state = self.converged_state()
class FiftyBranchProtectionsTransport(FakeTransport):
def request(self, method, path, *, query=None, payload=None):
if method == "GET" and path.endswith("/branch_protections"):
self.calls.append((method, path, deepcopy(query), deepcopy(payload)))
branch_reads = [call for call in self.calls if call[1].endswith("/branch_protections")]
if len(branch_reads) > 1:
raise AssertionError("branch protections endpoint must be read only once")
return [
deepcopy(self.branch),
*({"rule_name": f"unmanaged-{index}"} for index in range(49)),
]
return super().request(method, path, query=query, payload=payload)
transport = FiftyBranchProtectionsTransport(state)
self.assertEqual(self.provisioner(transport).check(), [])
branch_reads = [call for call in transport.calls if call[1].endswith("/branch_protections")]
self.assertEqual(
branch_reads,
[
("GET", "/api/v1/repos/conductor/rune/branch_protections", None, None),
],
)
def test_normalization_accepts_equivalent_colors_and_instants(self):
self.assertEqual(normalize_color("#D73A4A"), "d73a4a")
self.assertEqual(normalize_instant("2026-08-31T01:00:00+01:00"), "2026-08-31T00:00:00Z")
state = self.converged_state()
state["labels"][0]["color"] = "d73a4a"
state["milestones"][0]["due_on"] = "2026-08-31T01:00:00+01:00"
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).check(), [])
def test_apply_repairs_malformed_live_label_color_and_milestone_date(self):
state = self.converged_state()
state["labels"][0]["color"] = "not-a-color"
state["milestones"][0]["due_on"] = "not-a-date"
transport = FakeTransport(state)
self.assertEqual(self.provisioner(transport).apply(), [])
self.assertEqual(transport.labels[0]["color"], "d73a4a")
self.assertEqual(transport.milestones[0]["due_on"], "2026-08-31T00:00:00Z")
def test_manifest_is_validated_before_credentials_are_loaded(self):
manifest_path = self.root / "state.json"
invalid = deepcopy(self.manifest)
invalid["labels"][1]["name"] = invalid["labels"][0]["name"]
manifest_path.write_text(json.dumps(invalid), encoding="utf-8")
loaded = []
with self.assertRaises(ManifestError):
run(
["--manifest", str(manifest_path), "--check"],
token_loader=lambda: loaded.append(True),
)
self.assertEqual(loaded, [])
def test_manifest_rejects_unsafe_or_missing_wiki_sources(self):
manifest_path = self.root / "state.json"
invalid = json.loads(TRACKED_MANIFEST.read_text(encoding="utf-8"))
invalid["wiki_pages"][0]["source"] = "../secret"
manifest_path.write_text(json.dumps(invalid), encoding="utf-8")
with self.assertRaises(ManifestError):
load_manifest(manifest_path, repository_root=self.root)
def test_tracked_manifest_requires_all_managed_object_counts(self):
manifest = json.loads(TRACKED_MANIFEST.read_text(encoding="utf-8"))
manifest["labels"].pop()
manifest_path = self.root / "state.json"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
for page in manifest["wiki_pages"]:
source = self.root / page["source"]
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text("wiki", encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "exactly 11 labels"):
load_manifest(manifest_path, repository_root=self.root)
def test_manifest_requires_complete_branch_protection_policy(self):
manifest = json.loads(TRACKED_MANIFEST.read_text(encoding="utf-8"))
manifest["branch_protection"].pop("enable_force_push")
manifest_path = self.root / "state.json"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
for page in manifest["wiki_pages"]:
source = self.root / page["source"]
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text("wiki", encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "enable_force_push"):
load_manifest(manifest_path, repository_root=self.root)
def test_manifest_rejects_malformed_label_colors_and_milestone_dates(self):
cases = (
("labels", "color", "not-a-color", "invalid label color"),
("milestones", "due_on", "not-a-date", "invalid RFC3339 instant"),
)
for collection, field, value, message in cases:
with self.subTest(field=field):
manifest = json.loads(TRACKED_MANIFEST.read_text(encoding="utf-8"))
manifest[collection][0][field] = value
manifest_path = self.root / f"invalid-{field}.json"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
for page in manifest["wiki_pages"]:
source = self.root / page["source"]
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text("wiki", encoding="utf-8")
with self.assertRaisesRegex(ManifestError, message):
load_manifest(manifest_path, repository_root=self.root)
if __name__ == "__main__":
unittest.main()