"""Tests for scripts/create_release.py — idempotent release creation (#192). Pure tests cover payload construction and the tag-exists check; the dry-run mode is exercised end-to-end with monkeypatched env (no API calls). The script-level error path (finding 8, #192) runs the real entry point as a subprocess against a refused port. """ from __future__ import annotations import importlib.util import json import os import socket import subprocess import sys from pathlib import Path from typing import Any import pytest ROOT = Path(__file__).resolve().parents[1] def _load_module() -> Any: spec = importlib.util.spec_from_file_location("create_release", ROOT / "scripts" / "create_release.py") assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module cr = _load_module() class TestBuildReleasePayload: def test_with_pr_context(self) -> None: payload = cr.build_release_payload("0.56.0", "feat: widget", "Some body") assert payload == { "tag_name": "v0.56.0", "target_commitish": "master", "name": "Rune v0.56.0", "body": "## feat: widget\n\nSome body", "draft": False, "prerelease": False, } def test_without_pr_context_falls_back_to_safety_net_body(self) -> None: payload = cr.build_release_payload("0.56.0", "", "") assert payload["tag_name"] == "v0.56.0" assert "safety net" in payload["body"] assert payload["draft"] is False assert payload["prerelease"] is False class TestReleaseExists: def test_matches_tag_name(self) -> None: releases = [{"tag_name": "v0.54.0"}, {"tag_name": "v0.55.0"}] assert cr.release_exists(releases, "0.55.0") is True assert cr.release_exists(releases, "0.56.0") is False assert cr.release_exists([], "0.55.0") is False class TestURLErrorCaughtAtScriptLevel: """Finding 8 (#192): __main__ catches only HTTPError, so a refused connection (URLError) escapes and crashes with a traceback instead of the consistent error path. Runs the real entry point as a subprocess.""" def test_refused_connection_exits_1_with_message_not_traceback(self, tmp_path: Path) -> None: with socket.socket() as s: s.bind(("127.0.0.1", 0)) refused_port = s.getsockname()[1] # bound but never listening → ECONNREFUSED env = dict(os.environ) env.update( RELEASE_VERSION="0.56.0", GITEA_TOKEN="test-token", RELEASE_SERVER_URL=f"http://127.0.0.1:{refused_port}", RELEASE_REPO="rune/rune", LIST_API_URL=f"http://127.0.0.1:{refused_port}/api/v1/repos/rune/rune/releases?limit=50", RELEASE_API_URL=f"http://127.0.0.1:{refused_port}/api/v1/repos/rune/rune/releases", ) env.pop("PYTEST_CURRENT_TEST", None) proc = subprocess.run( [sys.executable, str(ROOT / "scripts" / "create_release.py")], env=env, capture_output=True, text=True, timeout=60, ) assert proc.returncode == 1, f"expected exit 1, stderr:\n{proc.stderr}" assert proc.stderr.startswith("create_release: API failed") assert "Traceback" not in proc.stderr class TestDryRunMain: def test_prints_payload_without_api_calls( self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.setenv("RELEASE_VERSION", "0.56.0") monkeypatch.delenv("GITEA_TOKEN", raising=False) monkeypatch.delenv("PR_TITLE", raising=False) rc = cr.main(["--dry-run"]) assert rc == 0 payload = json.loads(capsys.readouterr().out) assert payload["tag_name"] == "v0.56.0" assert payload["target_commitish"] == "master" assert "safety net" in payload["body"]