Release v0.1.2
This commit is contained in:
243
tests/test_module_system.py
Normal file
243
tests/test_module_system.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Keep the default app import side effect from bootstrapping a development DB.
|
||||
_TEST_ROOT = Path(tempfile.mkdtemp(prefix="govoplan-module-tests-"))
|
||||
os.environ.setdefault("APP_ENV", "test")
|
||||
os.environ.setdefault("DATABASE_URL", f"sqlite:///{_TEST_ROOT / 'module-test.db'}")
|
||||
os.environ.setdefault("DEV_BOOTSTRAP_ENABLED", "false")
|
||||
os.environ.setdefault("CELERY_ENABLED", "false")
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.core.migrations import migration_metadata_plan
|
||||
from govoplan_core.server.app import create_app
|
||||
from govoplan_core.server.config import GovoplanServerConfig
|
||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||
from govoplan_files.backend.storage.backends import LocalFilesystemStorageBackend, StorageBackendError
|
||||
|
||||
|
||||
def _settings(root: Path) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
app_env="test",
|
||||
database_url=f"sqlite:///{root / 'test.db'}",
|
||||
dev_mailbox_api_enabled=False,
|
||||
file_storage_backend="local",
|
||||
file_storage_local_root=str(root / "files"),
|
||||
file_storage_local_fallback_roots="",
|
||||
file_storage_s3_endpoint_url=None,
|
||||
file_storage_s3_region=None,
|
||||
file_storage_s3_bucket=None,
|
||||
file_storage_s3_access_key_id=None,
|
||||
file_storage_s3_secret_access_key=None,
|
||||
s3_endpoint_url=None,
|
||||
s3_region=None,
|
||||
s3_bucket=None,
|
||||
s3_access_key_id=None,
|
||||
s3_secret_access_key=None,
|
||||
file_upload_max_bytes=1024 * 1024,
|
||||
file_upload_zip_max_bytes=10 * 1024 * 1024,
|
||||
celery_enabled=False,
|
||||
redis_url="redis://127.0.0.1:6379/15",
|
||||
mock_mailbox_dir=str(root / "mock-mailbox"),
|
||||
)
|
||||
|
||||
|
||||
class ModuleSystemTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
shutil.rmtree(_TEST_ROOT, ignore_errors=True)
|
||||
|
||||
def _app_for_modules(self, modules: tuple[str, ...]):
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-module-app-", dir=_TEST_ROOT))
|
||||
settings = _settings(root)
|
||||
config = GovoplanServerConfig(
|
||||
title="GovOPlaN Module Test",
|
||||
version="test",
|
||||
settings=settings,
|
||||
enabled_modules=modules,
|
||||
base_routers=(),
|
||||
post_module_routers=(),
|
||||
extra_routers=(),
|
||||
lifespan=None,
|
||||
app_configurators=(),
|
||||
)
|
||||
return create_app(config), settings
|
||||
|
||||
def test_discovers_installed_core_and_product_module_manifests(self) -> None:
|
||||
manifests = available_module_manifests()
|
||||
self.assertTrue({"access", "files", "mail", "campaigns"}.issubset(manifests))
|
||||
self.assertEqual(manifests["campaigns"].dependencies, ("access",))
|
||||
self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail"))
|
||||
|
||||
def test_enabled_module_permutations_register_expected_routes(self) -> None:
|
||||
cases = (
|
||||
("core_only", (), {"access"}, set()),
|
||||
("files_only", ("files",), {"access", "files"}, {"/api/v1/files"}),
|
||||
("mail_only", ("mail",), {"access", "mail"}, {"/api/v1/mail"}),
|
||||
("campaign_without_files_or_mail", ("campaigns",), {"access", "campaigns"}, {"/api/v1/campaigns"}),
|
||||
("campaign_without_files", ("campaigns", "mail"), {"access", "campaigns", "mail"}, {"/api/v1/campaigns", "/api/v1/mail"}),
|
||||
("campaign_with_files_but_no_mail", ("campaigns", "files"), {"access", "campaigns", "files"}, {"/api/v1/campaigns", "/api/v1/files"}),
|
||||
("full_product", ("campaigns", "files", "mail"), {"access", "campaigns", "files", "mail"}, {"/api/v1/campaigns", "/api/v1/files", "/api/v1/mail"}),
|
||||
)
|
||||
for name, enabled_modules, expected_modules, expected_prefixes in cases:
|
||||
with self.subTest(name=name):
|
||||
app, _settings_obj = self._app_for_modules(enabled_modules)
|
||||
route_paths = {getattr(route, "path", "") for route in app.routes}
|
||||
self.assertIn("/api/v1/platform/modules", route_paths)
|
||||
for prefix in ("/api/v1/campaigns", "/api/v1/files", "/api/v1/mail"):
|
||||
has_prefix = any(path == prefix or path.startswith(f"{prefix}/") for path in route_paths)
|
||||
self.assertEqual(prefix in expected_prefixes, has_prefix, f"{name}: {prefix}")
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
payload_modules = {item["id"] for item in response.json()["modules"]}
|
||||
self.assertEqual(expected_modules, payload_modules)
|
||||
|
||||
|
||||
def _run_physical_absence_probe(self, *, enabled_modules: tuple[str, ...], blocked_modules: tuple[str, ...]) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-absence-probe-", dir=_TEST_ROOT))
|
||||
enabled = ",".join(("access", *enabled_modules)) if enabled_modules else "access"
|
||||
script = f"""
|
||||
import importlib.abc
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["APP_ENV"] = "test"
|
||||
os.environ["DATABASE_URL"] = {f"sqlite:///{root / 'probe.db'}"!r}
|
||||
os.environ["DEV_BOOTSTRAP_ENABLED"] = "false"
|
||||
os.environ["CELERY_ENABLED"] = "false"
|
||||
os.environ["ENABLED_MODULES"] = {enabled!r}
|
||||
|
||||
BLOCKED = {blocked_modules!r}
|
||||
|
||||
class Blocker(importlib.abc.MetaPathFinder):
|
||||
def find_spec(self, fullname, path=None, target=None):
|
||||
for prefix in BLOCKED:
|
||||
if fullname == prefix or fullname.startswith(prefix + "."):
|
||||
raise ModuleNotFoundError(f"No module named {{prefix!r}}", name=prefix)
|
||||
return None
|
||||
|
||||
sys.meta_path.insert(0, Blocker())
|
||||
|
||||
from govoplan_core.server.app import create_app
|
||||
from govoplan_core.server.config import GovoplanServerConfig
|
||||
from govoplan_core.server.registry import build_platform_registry
|
||||
|
||||
registry = build_platform_registry({enabled_modules!r})
|
||||
config = GovoplanServerConfig(
|
||||
title="absence probe",
|
||||
version="test",
|
||||
enabled_modules={enabled_modules!r},
|
||||
base_routers=(),
|
||||
post_module_routers=(),
|
||||
extra_routers=(),
|
||||
lifespan=None,
|
||||
app_configurators=(),
|
||||
)
|
||||
app = create_app(config)
|
||||
route_paths = [getattr(route, "path", "") for route in app.routes]
|
||||
print(json.dumps({{"modules": [item.id for item in registry.manifests()], "routes": route_paths}}, sort_keys=True))
|
||||
"""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", script],
|
||||
cwd=str(Path(__file__).resolve().parents[1]),
|
||||
env=os.environ.copy(),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
self.fail(result.stderr or result.stdout)
|
||||
|
||||
def test_campaign_optional_integrations_follow_enabled_modules(self) -> None:
|
||||
cases = (
|
||||
(("campaigns",), False, False),
|
||||
(("campaigns", "mail"), False, True),
|
||||
(("campaigns", "files"), True, False),
|
||||
(("campaigns", "files", "mail"), True, True),
|
||||
)
|
||||
for enabled_modules, files_enabled, mail_enabled in cases:
|
||||
with self.subTest(enabled_modules=enabled_modules):
|
||||
registry = build_platform_registry(enabled_modules)
|
||||
self.assertTrue(registry.has_module("campaigns"))
|
||||
self.assertEqual(files_enabled, registry.integration_enabled("campaigns", "files"))
|
||||
self.assertEqual(mail_enabled, registry.integration_enabled("campaigns", "mail"))
|
||||
|
||||
|
||||
def test_module_permutations_start_when_absent_modules_are_physically_unavailable(self) -> None:
|
||||
cases = (
|
||||
((), ("govoplan_files", "govoplan_mail", "govoplan_campaign")),
|
||||
(("files",), ("govoplan_mail", "govoplan_campaign")),
|
||||
(("mail",), ("govoplan_files", "govoplan_campaign")),
|
||||
(("campaigns",), ("govoplan_files", "govoplan_mail")),
|
||||
(("campaigns", "files"), ("govoplan_mail",)),
|
||||
(("campaigns", "mail"), ("govoplan_files",)),
|
||||
(("campaigns", "files", "mail"), ()),
|
||||
)
|
||||
for enabled_modules, blocked_modules in cases:
|
||||
with self.subTest(enabled_modules=enabled_modules, blocked_modules=blocked_modules):
|
||||
self._run_physical_absence_probe(enabled_modules=enabled_modules, blocked_modules=blocked_modules)
|
||||
|
||||
def test_module_route_factories_receive_runtime_settings(self) -> None:
|
||||
app, settings = self._app_for_modules(("files", "mail"))
|
||||
self.assertIsNotNone(app)
|
||||
|
||||
from govoplan_files.backend.runtime import get_settings as get_files_settings
|
||||
from govoplan_mail.backend.runtime import get_settings as get_mail_settings
|
||||
|
||||
self.assertIs(settings, get_files_settings())
|
||||
self.assertIs(settings, get_mail_settings())
|
||||
|
||||
def test_module_migrations_are_registered_only_for_enabled_modules(self) -> None:
|
||||
core_plan = migration_metadata_plan(build_platform_registry(()))
|
||||
self.assertEqual((), core_plan.script_locations)
|
||||
self.assertEqual(1, len(core_plan.metadata))
|
||||
|
||||
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
|
||||
self.assertEqual(1, len(files_plan.script_locations))
|
||||
self.assertTrue(files_plan.script_locations[0].endswith("govoplan_files/backend/migrations/versions"))
|
||||
|
||||
full_plan = migration_metadata_plan(build_platform_registry(("campaigns", "files", "mail")))
|
||||
locations = "\n".join(full_plan.script_locations)
|
||||
self.assertIn("govoplan_campaign/backend/migrations/versions", locations)
|
||||
self.assertIn("govoplan_files/backend/migrations/versions", locations)
|
||||
self.assertIn("govoplan_mail/backend/migrations/versions", locations)
|
||||
|
||||
def test_local_storage_backend_reads_from_fallback_roots_without_copying(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-storage-fallback-") as directory:
|
||||
root = Path(directory) / "primary"
|
||||
fallback = Path(directory) / "fallback"
|
||||
key = "tenant-a/files/demo.txt"
|
||||
(fallback / key).parent.mkdir(parents=True)
|
||||
(fallback / key).write_bytes(b"fallback-data")
|
||||
|
||||
backend = LocalFilesystemStorageBackend(root=root, fallback_roots=(fallback,))
|
||||
|
||||
self.assertTrue(backend.exists(key))
|
||||
self.assertEqual(b"fallback-data", backend.get_bytes(key))
|
||||
self.assertEqual([b"fallback", b"-data"], list(backend.iter_bytes(key, chunk_size=8)))
|
||||
self.assertFalse((root / key).exists())
|
||||
|
||||
backend.delete(key)
|
||||
self.assertTrue((fallback / key).exists())
|
||||
self.assertEqual(b"fallback-data", backend.get_bytes(key))
|
||||
|
||||
with self.assertRaises(StorageBackendError):
|
||||
backend.get_bytes("../escape.txt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user