feat: strengthen module contracts and shared WebUI runtime
This commit is contained in:
@@ -230,7 +230,10 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertTrue({"access", "admin", "tenancy", "policy", "audit", "dashboard", "files", "mail", "campaigns", "docs", "ops"}.issubset(manifests))
|
||||
self.assertEqual(manifests["campaigns"].dependencies, ())
|
||||
self.assertTrue(manifests["campaigns"].required_capabilities)
|
||||
self.assertEqual(manifests["campaigns"].optional_dependencies, ("files", "mail", "notifications", "addresses"))
|
||||
self.assertEqual(
|
||||
manifests["campaigns"].optional_dependencies,
|
||||
("files", "mail", "notifications", "addresses", "postbox"),
|
||||
)
|
||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||
self.assertTrue(manifests["dashboard"].required_capabilities)
|
||||
self.assertEqual(manifests["docs"].dependencies, ())
|
||||
@@ -587,7 +590,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
|
||||
def test_enabled_module_permutations_register_expected_routes(self) -> None:
|
||||
cases = (
|
||||
("core_only", (), {"access"}, set()),
|
||||
("core_only", (), set(), 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"}),
|
||||
@@ -602,8 +605,14 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
app, _settings_obj = self._app_for_modules(enabled_modules)
|
||||
route_paths = _route_paths(app)
|
||||
self.assertIn("/api/v1/platform/modules", route_paths)
|
||||
self.assertIn("/api/v1/auth/login", route_paths)
|
||||
self.assertIn("/api/v1/admin/users", route_paths)
|
||||
self.assertEqual(
|
||||
"access" in expected_modules,
|
||||
"/api/v1/auth/login" in route_paths,
|
||||
)
|
||||
self.assertEqual(
|
||||
"access" in expected_modules,
|
||||
"/api/v1/admin/users" in route_paths,
|
||||
)
|
||||
for prefix in ("/api/v1/campaigns", "/api/v1/files", "/api/v1/mail", "/api/v1/docs", "/api/v1/ops"):
|
||||
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}")
|
||||
@@ -613,12 +622,19 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertEqual(health.status_code, 200, health.text)
|
||||
self.assertEqual({"status": "ok"}, health.json())
|
||||
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(
|
||||
401 if "access" in expected_modules else 503,
|
||||
response.status_code,
|
||||
response.text,
|
||||
)
|
||||
payload_modules = {
|
||||
item.id
|
||||
for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual(expected_modules, payload_modules)
|
||||
|
||||
def test_governance_template_routes_are_contributed_by_admin_module(self) -> None:
|
||||
access_only_app, _settings_obj = self._app_for_modules(())
|
||||
access_only_app, _settings_obj = self._app_for_modules(("access",))
|
||||
access_only_paths = _route_paths(access_only_app)
|
||||
self.assertIn("/api/v1/admin/users", access_only_paths)
|
||||
self.assertNotIn("/api/v1/admin/system/governance-templates", access_only_paths)
|
||||
@@ -827,6 +843,37 @@ finally:
|
||||
with self.assertRaisesRegex(RegistryError, "requires unavailable capability"):
|
||||
registry.validate()
|
||||
|
||||
def test_registry_keeps_optional_auth_modules_access_free(self) -> None:
|
||||
for modules in ((), ("poll",), ("evaluation",), ("scheduling",)):
|
||||
with self.subTest(modules=modules):
|
||||
registry = build_platform_registry(modules)
|
||||
self.assertFalse(registry.has_module("access"))
|
||||
|
||||
def test_registry_prefers_selected_auth_provider_over_default(self) -> None:
|
||||
provider = ModuleManifest(
|
||||
id="alternative_auth",
|
||||
name="Alternative auth",
|
||||
version="test",
|
||||
capability_factories={
|
||||
"auth.principalResolver": lambda _context: object(),
|
||||
},
|
||||
)
|
||||
consumer = ModuleManifest(
|
||||
id="consumer",
|
||||
name="Consumer",
|
||||
version="test",
|
||||
required_capabilities=("auth.principalResolver",),
|
||||
)
|
||||
|
||||
registry = build_platform_registry(
|
||||
("alternative_auth", "consumer"),
|
||||
manifest_factories=(lambda: provider, lambda: consumer),
|
||||
)
|
||||
|
||||
self.assertFalse(registry.has_module("access"))
|
||||
self.assertTrue(registry.has_module("alternative_auth"))
|
||||
self.assertTrue(registry.has_module("consumer"))
|
||||
|
||||
def test_registry_resolves_required_interface_ranges(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
registry.register(ModuleManifest(
|
||||
@@ -903,7 +950,7 @@ finally:
|
||||
plan = plan_desired_enabled_modules(("campaigns",), manifests)
|
||||
|
||||
self.assertEqual(("access", "admin", "campaigns"), plan.enabled_modules)
|
||||
self.assertEqual((), plan.added_dependencies)
|
||||
self.assertEqual(("access",), plan.added_dependencies)
|
||||
|
||||
def test_module_install_plan_is_saved_alongside_desired_state(self) -> None:
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-module-install-plan-", dir=_TEST_ROOT))
|
||||
@@ -3300,7 +3347,10 @@ finally:
|
||||
"version_min": "0.2.0",
|
||||
"version_max_exclusive": "0.3.0",
|
||||
}, modules["campaigns"]["requires_interfaces"])
|
||||
self.assertEqual(["files", "mail", "notifications", "addresses"], modules["campaigns"]["optional_dependencies"])
|
||||
self.assertEqual(
|
||||
["files", "mail", "notifications", "addresses", "postbox"],
|
||||
modules["campaigns"]["optional_dependencies"],
|
||||
)
|
||||
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
||||
self.assertIn("migration", modules["files"]["migration_notes"].lower())
|
||||
self.assertEqual("0.1.9", modules["files"]["version"])
|
||||
@@ -3946,8 +3996,10 @@ finally:
|
||||
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(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "files"}, payload_modules)
|
||||
|
||||
def test_create_app_ignores_saved_desired_modules_that_are_not_installed(self) -> None:
|
||||
@@ -3979,8 +4031,10 @@ finally:
|
||||
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(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "files"}, payload_modules)
|
||||
|
||||
def test_create_app_preserves_admin_when_configured_and_saved_state_is_older(self) -> None:
|
||||
@@ -4012,8 +4066,10 @@ finally:
|
||||
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(response.status_code, 401, response.text)
|
||||
payload_modules = {
|
||||
item.id for item in app.state.govoplan_registry.manifests()
|
||||
}
|
||||
self.assertEqual({"access", "admin", "files"}, payload_modules)
|
||||
|
||||
def test_module_lifecycle_enables_and_disables_routes_without_restart(self) -> None:
|
||||
@@ -4023,27 +4079,37 @@ finally:
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 503, response.text)
|
||||
self.assertEqual((), lifecycle.active_module_ids())
|
||||
self.assertFalse("/api/v1/files" in _route_paths(app))
|
||||
|
||||
result = lifecycle.apply_enabled_modules(("files",), migrate=False)
|
||||
self.assertEqual(("files",), result.activated_modules)
|
||||
self.assertEqual(("access", "files"), result.activated_modules)
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access", "files"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 401, response.text)
|
||||
self.assertEqual(("access", "files"), lifecycle.active_module_ids())
|
||||
self.assertTrue("/api/v1/files" in _route_paths(app))
|
||||
self.assertEqual(401, client.get("/api/v1/files").status_code)
|
||||
|
||||
result = lifecycle.apply_enabled_modules((), migrate=False)
|
||||
self.assertEqual(("files",), result.deactivated_modules)
|
||||
self.assertEqual(("access", "files"), result.deactivated_modules)
|
||||
response = client.get("/api/v1/platform/modules")
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual({"access"}, {item["id"] for item in response.json()["modules"]})
|
||||
self.assertEqual(response.status_code, 503, response.text)
|
||||
self.assertEqual((), lifecycle.active_module_ids())
|
||||
disabled_response = client.get("/api/v1/files")
|
||||
self.assertEqual(404, disabled_response.status_code)
|
||||
self.assertEqual("Module is disabled: files", disabled_response.json()["detail"])
|
||||
|
||||
def test_module_lifecycle_live_apply_defaults_by_environment(self) -> None:
|
||||
app, settings = self._app_for_modules(())
|
||||
lifecycle = app.state.govoplan_lifecycle
|
||||
|
||||
self.assertTrue(lifecycle.live_apply_enabled())
|
||||
settings.app_env = "production"
|
||||
self.assertFalse(lifecycle.live_apply_enabled())
|
||||
settings.module_live_apply_enabled = True
|
||||
self.assertTrue(lifecycle.live_apply_enabled())
|
||||
|
||||
|
||||
def test_module_permutations_start_when_absent_modules_are_physically_unavailable(self) -> None:
|
||||
cases = (
|
||||
@@ -4073,10 +4139,9 @@ finally:
|
||||
|
||||
def test_module_migrations_are_registered_only_for_enabled_modules(self) -> None:
|
||||
core_plan = migration_metadata_plan(build_platform_registry(()))
|
||||
self.assertEqual(1, len(core_plan.script_locations))
|
||||
self.assertTrue(core_plan.script_locations[0].endswith("govoplan_access/backend/migrations/versions"))
|
||||
self.assertEqual(1, len(core_plan.metadata))
|
||||
self.assertEqual(("access",), tuple(item.module_id for item in core_plan.modules))
|
||||
self.assertEqual((), core_plan.script_locations)
|
||||
self.assertEqual((), core_plan.metadata)
|
||||
self.assertEqual((), core_plan.modules)
|
||||
|
||||
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
|
||||
self.assertEqual(2, len(files_plan.script_locations))
|
||||
|
||||
Reference in New Issue
Block a user