release: public alpha 0.6.0 baseline
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
|
||||
KERNEL_SRC = PROJECT_ROOT / "packages" / "jwebot-kernel" / "src"
|
||||
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
|
||||
|
||||
from jwebot_kernel import ( # noqa: E402
|
||||
ReleaseError,
|
||||
ReleaseRecord,
|
||||
build_release,
|
||||
parse_manifest,
|
||||
verify_release,
|
||||
)
|
||||
|
||||
|
||||
def manifest():
|
||||
return parse_manifest(
|
||||
{
|
||||
"id": "hello-command",
|
||||
"author": "tester",
|
||||
"description": "Say hello",
|
||||
"help": ["/hello"],
|
||||
"version": "1.0.0",
|
||||
"api_version": "1",
|
||||
"entrypoint": "hello_command:plugin",
|
||||
"type": "command",
|
||||
"capabilities": {
|
||||
"commands": ["/hello"],
|
||||
"storage": False,
|
||||
"network_profiles": [],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def extension(root: Path) -> Path:
|
||||
root.mkdir()
|
||||
(root / "src").mkdir()
|
||||
(root / "plugin.toml").write_text(
|
||||
'id = "hello-command"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "README.md").write_text("# hello\n", encoding="utf-8")
|
||||
(root / "src" / "hello_command.py").write_text(
|
||||
"\n".join(
|
||||
(
|
||||
"from jwebot_sdk import ExtensionResult, ReplyPlan",
|
||||
"",
|
||||
"def handle(event, arguments):",
|
||||
" return ExtensionResult(reply=ReplyPlan.text('hello'))",
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
class ReleaseTests(unittest.TestCase):
|
||||
def test_release_is_deterministic_across_mtime_changes(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
source = extension(base / "extension")
|
||||
first, _, first_record = build_release(
|
||||
source,
|
||||
base / "first",
|
||||
manifest=manifest(),
|
||||
)
|
||||
os.utime(source / "src" / "hello_command.py", (123456789, 123456789))
|
||||
second, _, second_record = build_release(
|
||||
source,
|
||||
base / "second",
|
||||
manifest=manifest(),
|
||||
)
|
||||
self.assertEqual(first.read_bytes(), second.read_bytes())
|
||||
self.assertEqual(first_record.source_digest, second_record.source_digest)
|
||||
self.assertEqual(
|
||||
first_record.artifact_sha256,
|
||||
second_record.artifact_sha256,
|
||||
)
|
||||
|
||||
def test_built_release_verifies_without_extraction(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
artifact, _, record = build_release(
|
||||
extension(base / "extension"),
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
verify_release(artifact, record)
|
||||
self.assertEqual(record.author, "tester")
|
||||
self.assertEqual(
|
||||
record.files,
|
||||
("README.md", "plugin.toml", "src/hello_command.py"),
|
||||
)
|
||||
|
||||
def test_artifact_tampering_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
artifact, _, record = build_release(
|
||||
extension(base / "extension"),
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
content = bytearray(artifact.read_bytes())
|
||||
content[-1] ^= 0x01
|
||||
artifact.write_bytes(content)
|
||||
with self.assertRaisesRegex(ReleaseError, "digest mismatch"):
|
||||
verify_release(artifact, record)
|
||||
|
||||
def test_release_record_rejects_invalid_author(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
_, _, record = build_release(
|
||||
extension(base / "extension"),
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
value = record.to_mapping()
|
||||
value["author"] = "../tester"
|
||||
with self.assertRaisesRegex(ReleaseError, "invalid release record"):
|
||||
ReleaseRecord.from_mapping(value)
|
||||
|
||||
def test_unsupported_source_file_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
source = extension(base / "extension")
|
||||
(source / "src" / "payload.bin").write_bytes(b"payload")
|
||||
with self.assertRaisesRegex(ReleaseError, "unsupported release file"):
|
||||
build_release(
|
||||
source,
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
|
||||
def test_interpreter_cache_is_excluded_from_release(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
source = extension(base / "extension")
|
||||
cache = source / "src" / "__pycache__"
|
||||
cache.mkdir()
|
||||
(cache / "hello_command.cpython-313.pyc").write_bytes(b"cache")
|
||||
|
||||
_, _, record = build_release(
|
||||
source,
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
record.files,
|
||||
("README.md", "plugin.toml", "src/hello_command.py"),
|
||||
)
|
||||
|
||||
def test_immutable_artifact_cannot_be_replaced(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
source = extension(base / "extension")
|
||||
artifact, _, _ = build_release(
|
||||
source,
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
artifact.write_bytes(b"tampered")
|
||||
with self.assertRaisesRegex(ReleaseError, "immutable release"):
|
||||
build_release(
|
||||
source,
|
||||
base / "output",
|
||||
manifest=manifest(),
|
||||
)
|
||||
|
||||
def test_unsafe_archive_member_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
artifact = Path(directory) / "unsafe.tar.gz"
|
||||
with artifact.open("wb") as raw:
|
||||
with gzip.GzipFile(
|
||||
filename="",
|
||||
mode="wb",
|
||||
fileobj=raw,
|
||||
mtime=0,
|
||||
) as compressed:
|
||||
with tarfile.open(fileobj=compressed, mode="w") as archive:
|
||||
content = b"unsafe"
|
||||
info = tarfile.TarInfo("plugin/../outside.py")
|
||||
info.size = len(content)
|
||||
archive.addfile(info, io.BytesIO(content))
|
||||
artifact_bytes = artifact.read_bytes()
|
||||
record = ReleaseRecord(
|
||||
plugin_id="hello-command",
|
||||
author="tester",
|
||||
plugin_version="1.0.0",
|
||||
api_version="1",
|
||||
source_digest="0" * 64,
|
||||
artifact_name=artifact.name,
|
||||
artifact_sha256=hashlib.sha256(artifact_bytes).hexdigest(),
|
||||
artifact_size=len(artifact_bytes),
|
||||
files=("outside.py",),
|
||||
)
|
||||
with self.assertRaisesRegex(ReleaseError, "unsafe member"):
|
||||
verify_release(artifact, record)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user