6f997bf118
test / unit (push) Successful in 1m0s
test / image-input-builds (push) Successful in 58s
test / integration-docker (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m6s
test / coverage (push) Successful in 14s
lint / lint (push) Successful in 3m47s
164 lines
6.7 KiB
Python
164 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
import urllib.error
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from bot_bottle.release_bundle import build_bundle_index, canonical_bytes
|
|
from bot_bottle.release_manifest import ReleaseManifestError
|
|
from bot_bottle.release_publish import (
|
|
bundle_url,
|
|
remote_bytes,
|
|
remote_sha256,
|
|
request,
|
|
upload as publish_artifact,
|
|
publish_bundle,
|
|
)
|
|
from tests.unit.test_release_manifest import manifest
|
|
|
|
|
|
class TestReleasePublish(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
env = {
|
|
"BOT_BOTTLE_RELEASE_BASE": "https://packages.example",
|
|
"BOT_BOTTLE_RELEASE_OWNER": "owner",
|
|
"BOT_BOTTLE_RELEASE_TOKEN": "token",
|
|
}
|
|
self.env = patch.dict("os.environ", env, clear=True)
|
|
self.env.start()
|
|
self.addCleanup(self.env.stop)
|
|
|
|
def bundle(self, root: Path) -> tuple[dict[str, object], Path]:
|
|
wheel = root / "bot_bottle-0.1.0-py3-none-any.whl"
|
|
wheel.write_bytes(b"wheel")
|
|
index = build_bundle_index(
|
|
manifest(),
|
|
wheel=wheel,
|
|
wheel_url=bundle_url("1" * 40, wheel.name),
|
|
workflow_run="actions/123",
|
|
published_at="2026-07-27T00:00:00Z",
|
|
)
|
|
return index, wheel
|
|
|
|
@staticmethod
|
|
def wheel_sha(index: dict[str, object]) -> str:
|
|
wheel = index["wheel"]
|
|
assert isinstance(wheel, dict)
|
|
digest = wheel["sha256"]
|
|
assert isinstance(digest, str)
|
|
return digest
|
|
|
|
def test_publishes_wheel_then_index(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
index, wheel = self.bundle(Path(tmp))
|
|
with patch(
|
|
"bot_bottle.release_publish.remote_bytes", return_value=None,
|
|
), patch(
|
|
"bot_bottle.release_publish.remote_sha256", return_value=None,
|
|
), patch("bot_bottle.release_publish.upload") as upload:
|
|
result = publish_bundle(index, wheel)
|
|
self.assertTrue(result.endswith("/bundle-index.json"))
|
|
self.assertEqual(2, upload.call_count)
|
|
self.assertIs(wheel, upload.call_args_list[0].args[1])
|
|
self.assertEqual(canonical_bytes(index), upload.call_args_list[1].args[1])
|
|
|
|
def test_exact_existing_bundle_is_idempotent(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
index, wheel = self.bundle(Path(tmp))
|
|
with patch(
|
|
"bot_bottle.release_publish.remote_bytes",
|
|
return_value=canonical_bytes(index),
|
|
), patch(
|
|
"bot_bottle.release_publish.remote_sha256",
|
|
return_value=self.wheel_sha(index),
|
|
), patch("bot_bottle.release_publish.upload") as upload:
|
|
publish_bundle(index, wheel)
|
|
upload.assert_not_called()
|
|
|
|
def test_rejects_partial_publication(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
index, wheel = self.bundle(Path(tmp))
|
|
with patch(
|
|
"bot_bottle.release_publish.remote_bytes", return_value=None,
|
|
), patch(
|
|
"bot_bottle.release_publish.remote_sha256",
|
|
return_value=self.wheel_sha(index),
|
|
):
|
|
with self.assertRaisesRegex(
|
|
ReleaseManifestError, "administrative cleanup",
|
|
):
|
|
publish_bundle(index, wheel)
|
|
|
|
def test_rejects_existing_bundle_with_different_index(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
index, wheel = self.bundle(Path(tmp))
|
|
with patch(
|
|
"bot_bottle.release_publish.remote_bytes", return_value=b"other",
|
|
), patch(
|
|
"bot_bottle.release_publish.remote_sha256",
|
|
return_value=self.wheel_sha(index),
|
|
):
|
|
with self.assertRaisesRegex(
|
|
ReleaseManifestError, "already differs",
|
|
):
|
|
publish_bundle(index, wheel)
|
|
|
|
def test_request_applies_auth_and_length(self) -> None:
|
|
result = request(
|
|
"https://packages.example/file",
|
|
method="PUT",
|
|
data=b"body",
|
|
length=4,
|
|
)
|
|
self.assertEqual(result.get_method(), "PUT")
|
|
self.assertEqual(result.get_header("Authorization"), "token token")
|
|
self.assertEqual(result.get_header("Content-length"), "4")
|
|
|
|
@patch("bot_bottle.release_publish.urllib.request.urlopen")
|
|
def test_remote_helpers_read_response(self, urlopen: MagicMock) -> None:
|
|
response = urlopen.return_value.__enter__.return_value
|
|
response.read.side_effect = [b"body"]
|
|
self.assertEqual(
|
|
remote_bytes("https://packages.example/file"), b"body")
|
|
response.read.side_effect = [b"body", b""]
|
|
self.assertEqual(
|
|
remote_sha256("https://packages.example/file"),
|
|
"230d8358dc8e8890b4c58deeb62912ee2"
|
|
"f20357ae92a5cc861b98e68fe31acb5",
|
|
)
|
|
|
|
@patch("bot_bottle.release_publish.urllib.request.urlopen")
|
|
def test_remote_helpers_handle_not_found(self, urlopen: MagicMock) -> None:
|
|
urlopen.side_effect = urllib.error.HTTPError(
|
|
"url", 404, "missing", MagicMock(), None)
|
|
self.assertIsNone(remote_bytes("https://packages.example/file"))
|
|
self.assertIsNone(remote_sha256("https://packages.example/file"))
|
|
|
|
@patch("bot_bottle.release_publish.urllib.request.urlopen")
|
|
def test_remote_helpers_report_registry_errors(
|
|
self, urlopen: MagicMock,
|
|
) -> None:
|
|
urlopen.side_effect = urllib.error.URLError("offline")
|
|
with self.assertRaisesRegex(ReleaseManifestError, "offline"):
|
|
remote_bytes("https://packages.example/file")
|
|
with self.assertRaisesRegex(ReleaseManifestError, "offline"):
|
|
remote_sha256("https://packages.example/file")
|
|
|
|
@patch("bot_bottle.release_publish.urllib.request.urlopen")
|
|
def test_upload_supports_bytes_and_files(self, urlopen: MagicMock) -> None:
|
|
urlopen.return_value.__enter__.return_value = MagicMock()
|
|
publish_artifact("https://packages.example/bytes", b"body")
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
source = Path(tmp) / "artifact"
|
|
source.write_bytes(b"artifact")
|
|
publish_artifact("https://packages.example/file", source)
|
|
self.assertEqual(urlopen.call_count, 2)
|
|
|
|
@patch("bot_bottle.release_publish.urllib.request.urlopen")
|
|
def test_upload_reports_failure(self, urlopen: MagicMock) -> None:
|
|
urlopen.side_effect = urllib.error.URLError("offline")
|
|
with self.assertRaisesRegex(ReleaseManifestError, "publishing"):
|
|
publish_artifact("https://packages.example/file", b"body")
|