157 lines
6.3 KiB
Python
157 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import io
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from support import FakeTransport, make_config, plugin_json, release_json, wheel_bytes
|
|
|
|
from netbox_store_agent.catalog import CatalogError, SecureHTTPTransport, StoreClient
|
|
from netbox_store_agent.config import StoreSettings
|
|
|
|
|
|
class CatalogTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temporary.name)
|
|
self.config = make_config(self.root)
|
|
|
|
def tearDown(self) -> None:
|
|
self.temporary.cleanup()
|
|
|
|
def test_trailing_slash_fallback_and_valid_wheel(self) -> None:
|
|
artifact = wheel_bytes(self.root)
|
|
release = release_json(
|
|
sha256=hashlib.sha256(artifact).hexdigest(), artifact_size=len(artifact)
|
|
)
|
|
responses = {
|
|
"http://store.test/api/v1/plugins/demo-plugin/": plugin_json(),
|
|
"http://store.test/api/v1/plugins/demo-plugin/releases/1.2.3/": release,
|
|
}
|
|
transport = FakeTransport(responses, artifact)
|
|
client = StoreClient(self.config, transport)
|
|
plan = client.get_release("demo-plugin", "1.2.3")
|
|
# Destination directory is caller-owned; use an existing operation directory.
|
|
operation_dir = self.root / "operation"
|
|
operation_dir.mkdir()
|
|
downloaded = client.download_release(plan, operation_dir)
|
|
self.assertTrue(downloaded.is_file())
|
|
self.assertEqual(transport.json_urls[0][-1], "n")
|
|
self.assertEqual(transport.json_urls[1][-1], "/")
|
|
|
|
def test_release_falls_back_to_plugin_releases_array(self) -> None:
|
|
release = release_json()
|
|
transport = FakeTransport(
|
|
{"http://store.test/api/v1/plugins/demo-plugin": plugin_json([release])}
|
|
)
|
|
plan = StoreClient(self.config, transport).get_release("demo-plugin", "1.2.3")
|
|
self.assertEqual(plan.version, "1.2.3")
|
|
|
|
def test_commit_bound_source_archive_is_verified(self) -> None:
|
|
buffer = io.BytesIO()
|
|
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
|
|
content = b"[build-system]\nrequires = []\n"
|
|
info = tarfile.TarInfo("demo/pyproject.toml")
|
|
info.size = len(content)
|
|
archive.addfile(info, io.BytesIO(content))
|
|
artifact = buffer.getvalue()
|
|
commit = "d" * 40
|
|
release = release_json(
|
|
download_url=f"http://artifacts.test/demo/archive/{commit}.tar.gz",
|
|
artifact_kind="source_archive",
|
|
artifact_filename="demo-plugin-1.2.3-source.tar.gz",
|
|
commit_sha=commit,
|
|
sha256=hashlib.sha256(artifact).hexdigest(),
|
|
artifact_size=len(artifact),
|
|
)
|
|
transport = FakeTransport(
|
|
{
|
|
"http://store.test/api/v1/plugins/demo-plugin": plugin_json(),
|
|
"http://store.test/api/v1/plugins/demo-plugin/releases/1.2.3": release,
|
|
},
|
|
artifact,
|
|
)
|
|
client = StoreClient(self.config, transport)
|
|
plan = client.get_release("demo-plugin", "1.2.3")
|
|
directory = self.root / "source-operation"
|
|
directory.mkdir()
|
|
self.assertEqual(client.download_release(plan, directory).read_bytes(), artifact)
|
|
|
|
def test_unapproved_or_mutable_release_rejected(self) -> None:
|
|
for change in ({"approved": False}, {"immutable": False}, {"status": "pending"}):
|
|
with self.subTest(change=change):
|
|
transport = FakeTransport(
|
|
{
|
|
"http://store.test/api/v1/plugins/demo-plugin": plugin_json(),
|
|
"http://store.test/api/v1/plugins/demo-plugin/releases/1.2.3": release_json(
|
|
**change
|
|
),
|
|
}
|
|
)
|
|
with self.assertRaises(CatalogError):
|
|
StoreClient(self.config, transport).get_release("demo-plugin", "1.2.3")
|
|
|
|
def test_unknown_catalog_field_fails_closed(self) -> None:
|
|
payload = plugin_json()
|
|
payload["internal_id"] = 7
|
|
transport = FakeTransport({"http://store.test/api/v1/plugins/demo-plugin": payload})
|
|
with self.assertRaises(CatalogError):
|
|
StoreClient(self.config, transport).get_plugin("demo-plugin")
|
|
|
|
def test_netbox_incompatibility_rejected(self) -> None:
|
|
transport = FakeTransport(
|
|
{
|
|
"http://store.test/api/v1/plugins/demo-plugin": plugin_json(
|
|
min_netbox_version="4.7.0"
|
|
)
|
|
}
|
|
)
|
|
with self.assertRaises(CatalogError):
|
|
StoreClient(self.config, transport).get_plugin("demo-plugin")
|
|
|
|
def test_wheel_distribution_must_match(self) -> None:
|
|
artifact = wheel_bytes(self.root)
|
|
release = release_json(
|
|
sha256=hashlib.sha256(artifact).hexdigest(), artifact_size=len(artifact)
|
|
)
|
|
payload = plugin_json(package_name="another-package")
|
|
transport = FakeTransport(
|
|
{
|
|
"http://store.test/api/v1/plugins/demo-plugin": payload,
|
|
"http://store.test/api/v1/plugins/demo-plugin/releases/1.2.3": release,
|
|
},
|
|
artifact,
|
|
)
|
|
client = StoreClient(self.config, transport)
|
|
plan = client.get_release("demo-plugin", "1.2.3")
|
|
directory = self.root / "mismatch"
|
|
directory.mkdir()
|
|
with self.assertRaises(CatalogError):
|
|
client.download_release(plan, directory)
|
|
|
|
def test_bearer_token_is_not_sent_to_artifact_origin(self) -> None:
|
|
settings = StoreSettings(
|
|
**{
|
|
**self.config.store.__dict__,
|
|
"base_url": "https://store.test:8443",
|
|
"allow_http_for_testing": False,
|
|
}
|
|
)
|
|
transport = SecureHTTPTransport(settings)
|
|
transport._token = lambda: "secret" # type: ignore[method-assign]
|
|
self.assertEqual(
|
|
transport._headers("https://store.test:8443/api")["Authorization"],
|
|
"Bearer secret",
|
|
)
|
|
self.assertNotIn(
|
|
"Authorization", transport._headers("https://artifacts.test/release.whl")
|
|
)
|
|
self.assertNotIn("Authorization", transport._headers("https://store.test/api"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|