532 lines
22 KiB
Python
532 lines
22 KiB
Python
"""Real Tauri signature checks using disposable keys, never release credentials."""
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import plistlib
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import release
|
|
from release_assets_test import DraftAssetGateTests # noqa: F401; unittest discovery
|
|
from release_gate_test import ReleaseGateTests # noqa: F401; unittest discovery
|
|
import release_formats
|
|
import release_github
|
|
|
|
ROOT = Path(os.environ.get("RELEASE_TEST_ROOT", Path(__file__).resolve().parents[1]))
|
|
|
|
|
|
def fixture_pe(machine=0x8664, magic=0x20B):
|
|
data = bytearray(256)
|
|
data[:2] = b"MZ"
|
|
struct.pack_into("<I", data, 60, 128)
|
|
data[128:132] = b"PE\0\0"
|
|
struct.pack_into("<H", data, 132, machine)
|
|
struct.pack_into("<H", data, 152, magic)
|
|
marker = b"__TAURI_BUNDLE_TYPE_VAR_UNK"
|
|
data[200 : 200 + len(marker)] = marker
|
|
return bytes(data)
|
|
|
|
|
|
def fixture_tar(files):
|
|
stream = io.BytesIO()
|
|
with tarfile.open(fileobj=stream, mode="w:gz") as archive:
|
|
for name, data in files.items():
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(data)
|
|
archive.addfile(info, io.BytesIO(data))
|
|
return stream.getvalue()
|
|
|
|
|
|
def fixture_deb(architecture="amd64", version="0.3.0"):
|
|
control = fixture_tar(
|
|
{
|
|
"./control": f"Package: shacraft-launcher\nVersion: {version}\nArchitecture: {architecture}\n".encode()
|
|
}
|
|
)
|
|
result = bytearray(b"!<arch>\n")
|
|
for name, data in [
|
|
("debian-binary", b"2.0\n"),
|
|
("control.tar.gz", control),
|
|
("data.tar.gz", fixture_tar({})),
|
|
]:
|
|
header = f"{name + '/':<16}{0:<12}{0:<6}{0:<6}{100644:<8}{len(data):<10}`\n".encode()
|
|
assert len(header) == 60
|
|
result.extend(header + data + (b"\n" if len(data) % 2 else b""))
|
|
return bytes(result)
|
|
|
|
|
|
def fixture_package(name, version="0.3.0"):
|
|
if name.endswith(".app.tar.gz"):
|
|
cpu = 0x0100000C if "aarch64" in name else 0x01000007
|
|
executable = struct.pack("<IIIIIIII", 0xFEEDFACF, cpu, 0, 2, 0, 0, 0, 0)
|
|
return fixture_tar(
|
|
{
|
|
"ShaCraft.app/Contents/Info.plist": plistlib.dumps(
|
|
{
|
|
"CFBundleExecutable": "shacraft-launcher",
|
|
"CFBundleShortVersionString": version,
|
|
}
|
|
),
|
|
"ShaCraft.app/Contents/MacOS/shacraft-launcher": executable,
|
|
}
|
|
)
|
|
if name.endswith(".AppImage"):
|
|
data = bytearray(64)
|
|
data[:6] = b"\x7fELF\x02\x01"
|
|
data[8:11] = b"AI\x02"
|
|
struct.pack_into("<H", data, 18, 62)
|
|
return bytes(data)
|
|
if name.endswith(".deb"):
|
|
return fixture_deb(version=version)
|
|
if name.endswith(".dmg"):
|
|
return b"koly" + bytes(508)
|
|
if name.endswith(".msi"):
|
|
data = bytearray(512)
|
|
data[:8] = bytes.fromhex("d0cf11e0a1b11ae1")
|
|
data[28:30] = b"\xfe\xff"
|
|
return bytes(data)
|
|
return fixture_pe(0x14C, 0x10B) # x86 NSIS wrapper is valid for an x64 payload.
|
|
|
|
|
|
class ReleaseTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.temporary = tempfile.TemporaryDirectory(prefix="shacraft-release-tests-")
|
|
cls.base = Path(cls.temporary.name)
|
|
# A new unencrypted disposable key per test process. No static private
|
|
# key or credentials are stored in the repository or test output.
|
|
cls.key = cls.base / "test-only.key"
|
|
environment = dict(os.environ)
|
|
for name in (
|
|
"TAURI_SIGNING_PRIVATE_KEY",
|
|
"TAURI_SIGNING_PRIVATE_KEY_PATH",
|
|
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD",
|
|
):
|
|
environment.pop(name, None)
|
|
result = subprocess.run(
|
|
[
|
|
"node",
|
|
str(ROOT / "node_modules/@tauri-apps/cli/tauri.js"),
|
|
"signer",
|
|
"generate",
|
|
"--ci",
|
|
"--password",
|
|
"",
|
|
"--write-keys",
|
|
str(cls.key),
|
|
],
|
|
env=environment,
|
|
capture_output=True,
|
|
timeout=120,
|
|
check=False,
|
|
)
|
|
if result.returncode:
|
|
raise RuntimeError("disposable Tauri signer setup failed")
|
|
cls.key.chmod(0o600)
|
|
cls.key_values = {
|
|
"SHACRAFT_UPDATER_PUBLIC_KEY": Path(str(cls.key) + ".pub").read_text().strip(),
|
|
"TAURI_SIGNING_PRIVATE_KEY": cls.key.read_text().strip(),
|
|
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD": "",
|
|
"SHACRAFT_UPDATER_TEST_BUILD": "0",
|
|
}
|
|
cls.originals = cls.base / "complete"
|
|
cls.originals.mkdir()
|
|
with patch.dict(os.environ, cls.key_values):
|
|
for name in release.expected_names("0.3.0"):
|
|
path = cls.originals / name
|
|
path.write_bytes(fixture_package(name))
|
|
release.signer(ROOT, path)
|
|
data = release.metadata(
|
|
ROOT, cls.originals, "0.3.0", "v0.3.0", "Test only", "2026-09-09T00:00:00Z"
|
|
)
|
|
(cls.originals / "latest.json").write_bytes(release.canonical(data))
|
|
release.signer(ROOT, cls.originals / "latest.json")
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.temporary.cleanup()
|
|
|
|
def setUp(self):
|
|
self.case = tempfile.TemporaryDirectory(dir=self.base)
|
|
self.directory = Path(self.case.name) / "assets"
|
|
shutil.copytree(self.originals, self.directory)
|
|
self.environment = patch.dict(os.environ, self.key_values)
|
|
self.environment.start()
|
|
|
|
def tearDown(self):
|
|
self.environment.stop()
|
|
self.case.cleanup()
|
|
|
|
def verify(self):
|
|
release.verify_release(ROOT, self.directory, "0.3.0", "v0.3.0")
|
|
|
|
def resign_metadata(self, mutate):
|
|
path = self.directory / "latest.json"
|
|
data = json.loads(path.read_bytes())
|
|
mutate(data)
|
|
path.write_bytes(release.canonical(data))
|
|
release.signer(ROOT, path)
|
|
|
|
def test_complete_release_and_metadata_generation_are_deterministic(self):
|
|
self.verify()
|
|
first = release.metadata(
|
|
ROOT, self.directory, "0.3.0", "v0.3.0", "Test only", "2026-09-09T00:00:00Z"
|
|
)
|
|
self.assertEqual(release.canonical(first), (self.directory / "latest.json").read_bytes())
|
|
|
|
def test_package_bit_flip_fails_actual_plugin_verification(self):
|
|
path = self.directory / release.filename("0.3.0", "linux-x86_64", ".AppImage")
|
|
path.write_bytes(path.read_bytes() + b"tampered")
|
|
with self.assertRaisesRegex(ValueError, "invalid updater signature"):
|
|
self.verify()
|
|
|
|
def test_metadata_substitution_is_rejected_before_json_parsing(self):
|
|
(self.directory / "latest.json").write_bytes(b"not even JSON")
|
|
with self.assertRaisesRegex(ValueError, "invalid updater signature"):
|
|
self.verify()
|
|
|
|
def test_valid_signature_for_other_artifact_does_not_suffice(self):
|
|
linux = release.filename("0.3.0", "linux-x86_64", ".AppImage")
|
|
windows = release.filename("0.3.0", "windows-x86_64", "-setup.exe")
|
|
shutil.copyfile(self.directory / (windows + ".sig"), self.directory / (linux + ".sig"))
|
|
with self.assertRaisesRegex(ValueError, "invalid updater signature"):
|
|
self.verify()
|
|
|
|
def test_wrong_public_key_fails_real_verifier(self):
|
|
encoded = release.public_key()
|
|
import base64
|
|
|
|
lines = base64.b64decode(encoded).decode().splitlines()
|
|
raw = bytearray(base64.b64decode(lines[1]))
|
|
raw[-1] ^= 1
|
|
lines[1] = base64.b64encode(raw).decode()
|
|
wrong = base64.b64encode(("\n".join(lines) + "\n").encode()).decode()
|
|
with patch.dict(os.environ, {"SHACRAFT_UPDATER_PUBLIC_KEY": wrong}):
|
|
with self.assertRaisesRegex(ValueError, "invalid updater signature"):
|
|
self.verify()
|
|
|
|
def test_all_platforms_and_manual_packages_are_mandatory(self):
|
|
for name in sorted(release.expected_names("0.3.0")):
|
|
path = self.directory / (name + ".sig")
|
|
original = path.read_bytes()
|
|
path.unlink()
|
|
with self.subTest(name=name), self.assertRaisesRegex(ValueError, "asset set"):
|
|
self.verify()
|
|
path.write_bytes(original)
|
|
|
|
def test_extra_ci_marker_or_file_blocks_promotion(self):
|
|
(self.directory / "CI_NOT_FOR_RELEASE.txt").write_text("test key")
|
|
with self.assertRaisesRegex(ValueError, "asset set"):
|
|
self.verify()
|
|
|
|
def test_signed_metadata_must_match_tag_and_exact_source_assets(self):
|
|
alterations = [
|
|
lambda x: x.update(version="0.2.0"),
|
|
lambda x: x.update(tag="v0.4.0"),
|
|
lambda x: x["platforms"].pop("darwin-aarch64"),
|
|
lambda x: x["platforms"].update({"linux-aarch64": x["platforms"]["linux-x86_64"]}),
|
|
lambda x: x["manualPackages"].pop("windows-x86_64-msi"),
|
|
lambda x: x["platforms"]["linux-x86_64"].update(url="https://evil.invalid/package"),
|
|
lambda x: x["platforms"]["linux-x86_64"].update(
|
|
url=x["platforms"]["linux-x86_64"]["url"].replace("v0.3.0", "v0.2.0")
|
|
),
|
|
lambda x: x["platforms"]["linux-x86_64"].update(sha256="0" * 64),
|
|
lambda x: x["platforms"]["linux-x86_64"].update(size=True),
|
|
lambda x: x.update(pub_date="2026-09-09T00:00:00+03:00"),
|
|
lambda x: x.update(extra="not allowed"),
|
|
]
|
|
for index, alter in enumerate(alterations):
|
|
shutil.copyfile(self.originals / "latest.json", self.directory / "latest.json")
|
|
self.resign_metadata(alter)
|
|
with self.subTest(index=index), self.assertRaises(ValueError):
|
|
self.verify()
|
|
|
|
def test_duplicate_json_fields_are_rejected_even_if_signed(self):
|
|
path = self.directory / "latest.json"
|
|
path.write_bytes(
|
|
path.read_bytes().replace(
|
|
b'"schemaVersion": 1,', b'"schemaVersion": 1, "schemaVersion": 1,'
|
|
)
|
|
)
|
|
release.signer(ROOT, path)
|
|
with self.assertRaisesRegex(ValueError, "duplicate JSON"):
|
|
self.verify()
|
|
|
|
def test_collect_requires_generated_updater_sig_and_signs_manual_deb(self):
|
|
bundle = Path(self.case.name) / "bundle"
|
|
(bundle / "appimage").mkdir(parents=True)
|
|
(bundle / "deb").mkdir()
|
|
app = bundle / "appimage/Test.AppImage"
|
|
app.write_bytes(fixture_package("Test.AppImage"))
|
|
(bundle / "deb/Test.deb").write_bytes(fixture_package("Test.deb"))
|
|
output = Path(self.case.name) / "collected"
|
|
with self.assertRaisesRegex(ValueError, "missing generated updater signature"):
|
|
release.collect(ROOT, bundle, output, "linux-x86_64", "0.3.0")
|
|
shutil.rmtree(output)
|
|
release.signer(ROOT, app)
|
|
release.collect(ROOT, bundle, output, "linux-x86_64", "0.3.0")
|
|
self.assertEqual(len(list(output.iterdir())), 4)
|
|
for suffix in (".AppImage", ".deb"):
|
|
data = output / release.filename("0.3.0", "linux-x86_64", suffix)
|
|
release.verify_signature(ROOT, data, Path(str(data) + ".sig"))
|
|
|
|
def test_missing_key_fails_and_optional_password_never_prompts(self):
|
|
data = self.directory / "latest.json"
|
|
with patch.dict(os.environ, {"TAURI_SIGNING_PRIVATE_KEY": ""}):
|
|
with self.assertRaisesRegex(ValueError, "PRIVATE_KEY is missing"):
|
|
release.signer(ROOT, data)
|
|
with patch.dict(os.environ):
|
|
os.environ.pop("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", None)
|
|
release.signer(ROOT, data)
|
|
release.verify_signature(ROOT, data, Path(str(data) + ".sig"))
|
|
|
|
def test_release_preflight_rejects_placeholder_mismatch_and_test_mode(self):
|
|
source = Path(self.case.name) / "source"
|
|
(source / "src-tauri").mkdir(parents=True)
|
|
(source / "package.json").write_text('{"version":"0.3.0"}')
|
|
(source / "package-lock.json").write_text(
|
|
'{"version":"0.3.0","packages":{"":{"version":"0.3.0"}}}'
|
|
)
|
|
(source / "src-tauri/tauri.conf.json").write_text('{"version":"0.3.0"}')
|
|
(source / "src-tauri/Cargo.toml").write_text('[package]\nversion="0.3.0"\n')
|
|
(source / "src-tauri/updater-public-key.txt").write_text("UNCONFIGURED")
|
|
with self.assertRaisesRegex(ValueError, "differs from committed"):
|
|
release.preflight(source, "0.3.0", "v0.3.0", signing=True)
|
|
with patch.dict(os.environ, {"SHACRAFT_UPDATER_TEST_BUILD": "1"}):
|
|
with self.assertRaisesRegex(ValueError, "test keys cannot release"):
|
|
release.preflight(source, "0.3.0", "v0.3.0", signing=True)
|
|
(source / "package.json").write_text('{"version":"0.3.1"}')
|
|
with self.assertRaisesRegex(ValueError, "source versions disagree"):
|
|
release.preflight(source, "0.3.0", "v0.3.0")
|
|
|
|
|
|
class PackageFormatTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory(prefix="shacraft-format-tests-")
|
|
self.root = Path(self.temporary.name)
|
|
|
|
def tearDown(self):
|
|
self.temporary.cleanup()
|
|
|
|
def package(self, name, data=None):
|
|
path = self.root / name
|
|
path.write_bytes(fixture_package(name) if data is None else data)
|
|
return path
|
|
|
|
def test_mac_archive_binds_main_executable_cpu_and_version(self):
|
|
path = self.package("darwin-aarch64.app.tar.gz")
|
|
release_formats.mac_app(path, "darwin-aarch64", "0.3.0")
|
|
for platform, version in [("darwin-x86_64", "0.3.0"), ("darwin-aarch64", "0.2.0")]:
|
|
with self.subTest(platform=platform, version=version), self.assertRaises(ValueError):
|
|
release_formats.mac_app(path, platform, version)
|
|
|
|
def test_mac_archive_rejects_traversal_and_missing_binary(self):
|
|
for files in (
|
|
{"../Bad.app/Contents/Info.plist": b"bad"},
|
|
{
|
|
"ShaCraft.app/Contents/Info.plist": plistlib.dumps(
|
|
{"CFBundleExecutable": "missing", "CFBundleShortVersionString": "0.3.0"}
|
|
)
|
|
},
|
|
):
|
|
path = self.package("darwin-aarch64.app.tar.gz", fixture_tar(files))
|
|
with self.assertRaises(ValueError):
|
|
release_formats.mac_app(path, "darwin-aarch64", "0.3.0")
|
|
|
|
def test_appimage_rejects_arm64_wrong_class_and_type1(self):
|
|
path = self.package("linux-x86_64.AppImage")
|
|
release_formats.appimage(path)
|
|
for offset, data in [(18, struct.pack("<H", 183)), (4, b"\x01"), (10, b"\x01")]:
|
|
bad = bytearray(fixture_package(path.name))
|
|
bad[offset : offset + len(data)] = data
|
|
path.write_bytes(bad)
|
|
with self.assertRaises(ValueError):
|
|
release_formats.appimage(path)
|
|
|
|
def test_deb_control_binds_architecture_and_version(self):
|
|
path = self.package("linux-x86_64.deb")
|
|
release_formats.deb(path, "0.3.0")
|
|
for architecture, version in [("arm64", "0.3.0"), ("amd64", "0.2.0")]:
|
|
path.write_bytes(fixture_deb(architecture, version))
|
|
with self.assertRaises(ValueError):
|
|
release_formats.deb(path, "0.3.0")
|
|
|
|
def test_nsis_stub_may_be_x86_but_extracted_main_must_equal_x64_build(self):
|
|
wrapper = self.package("setup.exe")
|
|
main = self.package("shacraft-launcher.exe", fixture_pe())
|
|
listing = subprocess.CompletedProcess(
|
|
[], 0, b"Path = setup.exe\nPath = $INSTDIR/shacraft-launcher.exe\n", b""
|
|
)
|
|
for extracted, success in [
|
|
(release_formats.expected_nsis_payload(fixture_pe()), True),
|
|
(fixture_pe(), False),
|
|
(release_formats.expected_nsis_payload(fixture_pe(0xAA64, 0x20B)), False),
|
|
(release_formats.expected_nsis_payload(fixture_pe()) + b"different", False),
|
|
]:
|
|
with (
|
|
patch.object(release_formats.shutil, "which", return_value=str(main)),
|
|
patch.object(
|
|
release_formats.subprocess,
|
|
"run",
|
|
side_effect=[listing, subprocess.CompletedProcess([], 0, extracted, b"")],
|
|
),
|
|
):
|
|
if success:
|
|
release_formats.windows(wrapper, "-setup.exe", "0.3.0", main)
|
|
else:
|
|
with self.assertRaises(ValueError):
|
|
release_formats.windows(wrapper, "-setup.exe", "0.3.0", main)
|
|
|
|
def test_nsis_identity_allows_only_the_actual_first_bundle_marker_patch(self):
|
|
original = fixture_pe() + b"__TAURI_BUNDLE_TYPE_VAR_UNK"
|
|
expected = release_formats.expected_nsis_payload(original)
|
|
self.assertEqual(
|
|
expected,
|
|
original.replace(b"__TAURI_BUNDLE_TYPE_VAR_UNK", b"__TAURI_BUNDLE_TYPE_VAR_NSS", 1),
|
|
)
|
|
self.assertEqual(expected.count(b"__TAURI_BUNDLE_TYPE_VAR_UNK"), 1)
|
|
with self.assertRaisesRegex(ValueError, "bundle-type marker"):
|
|
release_formats.expected_nsis_payload(
|
|
fixture_pe().replace(b"__TAURI_BUNDLE_TYPE_VAR_UNK", b"__TAURI_BUNDLE_TYPE_VAR_MSI")
|
|
)
|
|
# No certificate table/checksum/other byte range is ignored.
|
|
mutated = bytearray(expected)
|
|
mutated[190] ^= 1
|
|
self.assertNotEqual(bytes(mutated), release_formats.expected_nsis_payload(original))
|
|
self.assertNotEqual(
|
|
expected.replace(b"_VAR_NSS", b"_VAR_MSI"),
|
|
release_formats.expected_nsis_payload(original),
|
|
)
|
|
|
|
def test_msi_readonly_metadata_requires_x64_and_version(self):
|
|
msi = self.package("setup.msi")
|
|
main = self.package("shacraft-launcher.exe", fixture_pe())
|
|
for architecture, version, success in [
|
|
("x64;1033", "0.3.0", True),
|
|
("Intel;1033", "0.3.0", False),
|
|
("x64;1033", "0.2.0", False),
|
|
]:
|
|
result = subprocess.CompletedProcess(
|
|
[], 0, json.dumps({"template": architecture, "version": version}).encode(), b""
|
|
)
|
|
with patch.object(release_formats.subprocess, "run", return_value=result) as command:
|
|
if success:
|
|
release_formats.windows(msi, ".msi", "0.3.0", main)
|
|
else:
|
|
with self.assertRaises(ValueError):
|
|
release_formats.windows(msi, ".msi", "0.3.0", main)
|
|
self.assertIn("release_msi.ps1", command.call_args.args[0][4])
|
|
|
|
|
|
class WorkflowPolicyTests(unittest.TestCase):
|
|
def protected(self):
|
|
return {
|
|
"deployment_branch_policy": {
|
|
"protected_branches": True,
|
|
"custom_branch_policies": False,
|
|
},
|
|
"protection_rules": [
|
|
{
|
|
"type": "required_reviewers",
|
|
"prevent_self_review": True,
|
|
"reviewers": [{"type": "User", "reviewer": {"id": 123}}],
|
|
}
|
|
],
|
|
}
|
|
|
|
def test_protected_environment_requires_existing_independent_review(self):
|
|
release_github.validate_environment(self.protected())
|
|
for data in (
|
|
None,
|
|
{},
|
|
{"deployment_branch_policy": {}},
|
|
dict(self.protected(), protection_rules=[]),
|
|
):
|
|
with self.subTest(data=data), self.assertRaises(ValueError):
|
|
release_github.validate_environment(data)
|
|
for mutate in (
|
|
lambda d: d["deployment_branch_policy"].update(protected_branches=False),
|
|
lambda d: d["protection_rules"][0].update(prevent_self_review=False),
|
|
lambda d: d["protection_rules"][0].update(reviewers=[]),
|
|
):
|
|
data = self.protected()
|
|
mutate(data)
|
|
with self.assertRaises(ValueError):
|
|
release_github.validate_environment(data)
|
|
|
|
def test_gate_does_not_emit_missing_environment_name(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
output = Path(directory) / "output"
|
|
env = {
|
|
"GITHUB_REPOSITORY": release.REPOSITORY,
|
|
"GITHUB_EVENT_NAME": "workflow_dispatch",
|
|
"GITHUB_REF": "refs/heads/main",
|
|
"GITHUB_OUTPUT": str(output),
|
|
}
|
|
with (
|
|
patch.dict(os.environ, env),
|
|
patch.object(release_github, "api", side_effect=[{"protected": True}, None]),
|
|
):
|
|
with self.assertRaisesRegex(ValueError, "absent"):
|
|
release_github.gate("launcher-release")
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_publication_never_runs_without_exact_confirmation(self):
|
|
env = {
|
|
"GITHUB_REPOSITORY": release.REPOSITORY,
|
|
"GITHUB_EVENT_NAME": "workflow_dispatch",
|
|
"GITHUB_REF": "refs/heads/main",
|
|
}
|
|
with patch.dict(os.environ, env), patch.object(release_github, "gh") as client:
|
|
with self.assertRaisesRegex(ValueError, "confirmation"):
|
|
release_github.publish(ROOT, "0.3.0", "v0.3.0", "publish v0.2.0")
|
|
client.assert_not_called()
|
|
|
|
def test_asset_snapshot_rejects_public_or_incomplete_draft(self):
|
|
data = {
|
|
"draft": True,
|
|
"prerelease": False,
|
|
"assets": [
|
|
{
|
|
"id": 1,
|
|
"name": "latest.json",
|
|
"size": 10,
|
|
"state": "uploaded",
|
|
"digest": "sha256:abc",
|
|
}
|
|
],
|
|
}
|
|
self.assertEqual(release_github.asset_snapshot(data)["latest.json"][0], 1)
|
|
for change in (
|
|
{"draft": False},
|
|
{"prerelease": True},
|
|
{"assets": [dict(data["assets"][0], state="new")]},
|
|
{"assets": data["assets"] * 2},
|
|
):
|
|
with self.assertRaises(ValueError):
|
|
release_github.asset_snapshot(dict(data, **change))
|
|
|
|
def test_version_and_tag_cannot_inject_paths_or_exceed_msi_limits(self):
|
|
release.version_tag("0.3.0", "v0.3.0")
|
|
for version, tag in (
|
|
("0.3.0", "main"),
|
|
("0.3.0", "v0.3.0/evil"),
|
|
("01.3.0", "v01.3.0"),
|
|
("0.3.0-beta", "v0.3.0-beta"),
|
|
("256.0.0", "v256.0.0"),
|
|
("0.0.65536", "v0.0.65536"),
|
|
):
|
|
with self.subTest(version=version, tag=tag), self.assertRaises(ValueError):
|
|
release.version_tag(version, tag)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|