项目文件夹

文件
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

231 行
8.4 KiB
Python

"""Tests for user extension manifest scanner."""
from pathlib import Path
import yaml
from user_extensions import (
_reset_cache,
get_user_services_cached,
scan_user_extension_services,
)
def _write_manifest(ext_dir: Path, manifest: dict) -> None:
"""Write a manifest.yaml into the given extension directory."""
ext_dir.mkdir(parents=True, exist_ok=True)
(ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))
def _make_manifest(service_id: str, port: int = 8080, health: str = "/health",
name: str | None = None, default_host: str = "badhost") -> dict:
"""Build a minimal manifest dict."""
svc: dict = {"id": service_id, "port": port, "health": health,
"default_host": default_host}
if name is not None:
svc["name"] = name
return {"schema_version": "ods.services.v1", "service": svc}
# --- scan_user_extension_services ---
class TestScanUserExtensions:
def test_scan_empty_dir(self, tmp_path):
"""Empty directory returns empty dict."""
d = tmp_path / "user"
d.mkdir()
assert scan_user_extension_services(d) == {}
def test_scan_nonexistent_dir(self, tmp_path):
"""Non-existent directory returns empty dict."""
assert scan_user_extension_services(tmp_path / "nope") == {}
def test_scan_enabled_extension(self, tmp_path):
"""Extension with compose.yaml + manifest returns correct config."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext", port=9090,
health="/api/health",
name="My Extension"))
(ext_dir / "compose.yaml").write_text("services:\n my-ext:\n image: test\n")
result = scan_user_extension_services(user_dir)
assert "my-ext" in result
cfg = result["my-ext"]
assert cfg["host"] == "my-ext"
assert cfg["port"] == 9090
assert cfg["health"] == "/api/health"
assert cfg["name"] == "My Extension"
def test_scan_disabled_extension_skipped(self, tmp_path):
"""Extension with compose.yaml.disabled only is skipped."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext"))
(ext_dir / "compose.yaml.disabled").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result == {}
def test_scan_missing_manifest_skipped(self, tmp_path):
"""Extension without manifest.yaml is skipped."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
ext_dir.mkdir(parents=True)
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result == {}
def test_scan_no_health_endpoint_included_with_empty_health(self, tmp_path):
"""Extension without health field is included with empty health."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
ext_dir.mkdir(parents=True)
manifest = {"schema_version": "ods.services.v1",
"service": {"id": "my-ext", "port": 8080}}
(ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert "my-ext" in result
assert result["my-ext"]["health"] == ""
assert result["my-ext"]["port"] == 8080
def test_scan_health_path_validation(self, tmp_path):
"""Reject paths with .., @, ?, #, and scheme prefixes."""
user_dir = tmp_path / "user"
bad_paths = [
"/health/../etc/passwd",
"/health@evil.com",
"/health?cmd=exec",
"/health#fragment",
"http://evil.com/health",
"https://evil.com/health",
]
for i, bad_path in enumerate(bad_paths):
ext_id = f"ext-{i}"
ext_dir = user_dir / ext_id
_write_manifest(ext_dir, _make_manifest(ext_id, health=bad_path))
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result == {}, f"Expected all bad paths rejected, got: {list(result.keys())}"
def test_scan_host_is_service_id(self, tmp_path):
"""Returned host must be the directory name, not manifest default_host."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext",
default_host="evil.attacker.com"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result["my-ext"]["host"] == "my-ext"
def test_scan_name_fallback(self, tmp_path):
"""Extension without name in manifest falls back to service_id."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
# Manifest with no name field
manifest = {"schema_version": "ods.services.v1",
"service": {"id": "my-ext", "port": 8080, "health": "/health"}}
(ext_dir).mkdir(parents=True)
(ext_dir / "manifest.yaml").write_text(yaml.dump(manifest))
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result["my-ext"]["name"] == "my-ext"
def test_scan_symlink_skipped(self, tmp_path):
"""Symlinked directories in user-extensions are skipped."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "real-ext"
_write_manifest(ext_dir, _make_manifest("real-ext"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
(user_dir / "link-ext").symlink_to(ext_dir)
result = scan_user_extension_services(user_dir)
assert "real-ext" in result
assert "link-ext" not in result
def test_scan_invalid_service_id_skipped(self, tmp_path):
"""Directories with invalid service_id format are skipped."""
user_dir = tmp_path / "user"
for bad_name in ["UPPER", "-bad", "has spaces"]:
ext_dir = user_dir / bad_name
_write_manifest(ext_dir, _make_manifest("x", health="/health"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
result = scan_user_extension_services(user_dir)
assert result == {}
# --- Caching ---
class TestCaching:
def setup_method(self):
_reset_cache()
def teardown_method(self):
_reset_cache()
def test_cache_returns_same_result(self, tmp_path):
"""Second call within TTL returns cached result without rescanning."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
r1 = get_user_services_cached(user_dir, ttl=60.0)
assert "my-ext" in r1
# Remove the extension directory — cache should still return old result
import shutil
shutil.rmtree(ext_dir)
r2 = get_user_services_cached(user_dir, ttl=60.0)
assert r2 == r1
assert "my-ext" in r2
def test_cache_ttl_expires(self, tmp_path, monkeypatch):
"""After TTL, cache rescans the directory."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
r1 = get_user_services_cached(user_dir, ttl=0.0)
assert "my-ext" in r1
# Remove and call with ttl=0 (always expired)
import shutil
shutil.rmtree(ext_dir)
r2 = get_user_services_cached(user_dir, ttl=0.0)
assert r2 == {}
def test_reset_cache(self, tmp_path):
"""_reset_cache() clears cached data."""
user_dir = tmp_path / "user"
ext_dir = user_dir / "my-ext"
_write_manifest(ext_dir, _make_manifest("my-ext"))
(ext_dir / "compose.yaml").write_text("services: {}\n")
r1 = get_user_services_cached(user_dir, ttl=300.0)
assert "my-ext" in r1
_reset_cache()
# Remove the extension
import shutil
shutil.rmtree(ext_dir)
r2 = get_user_services_cached(user_dir, ttl=300.0)
assert r2 == {}