263 lines
8.1 KiB
Python
263 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from govoplan_core.auth import get_api_principal
|
|
from govoplan_core.core.modules import (
|
|
FrontendModule,
|
|
FrontendRoute,
|
|
ModuleManifest,
|
|
NavItem,
|
|
)
|
|
from govoplan_core.core.registry import (
|
|
PlatformRegistry,
|
|
RegistryError,
|
|
manifest_view_surfaces,
|
|
)
|
|
from govoplan_core.core.views import ViewSurface
|
|
from govoplan_core.server.platform import create_platform_router
|
|
from govoplan_core.server.registry import available_module_manifests
|
|
|
|
|
|
def example_manifest(
|
|
*,
|
|
custom_surfaces: tuple[ViewSurface, ...] = (),
|
|
) -> ModuleManifest:
|
|
return ModuleManifest(
|
|
id="example",
|
|
name="Example",
|
|
version="test",
|
|
frontend=FrontendModule(
|
|
module_id="example",
|
|
nav_items=(
|
|
NavItem(
|
|
path="/example",
|
|
label="Example",
|
|
order=20,
|
|
),
|
|
),
|
|
routes=(
|
|
FrontendRoute(
|
|
path="/example",
|
|
component="ExamplePage",
|
|
order=20,
|
|
),
|
|
),
|
|
view_surfaces=custom_surfaces,
|
|
),
|
|
)
|
|
|
|
|
|
class ViewSurfaceContractTests(unittest.TestCase):
|
|
def test_manifest_derives_module_navigation_and_route_surfaces(self) -> None:
|
|
surfaces = manifest_view_surfaces(example_manifest())
|
|
|
|
self.assertEqual(
|
|
[
|
|
"example.module",
|
|
"example.nav.example",
|
|
"example.route.example",
|
|
],
|
|
[surface.id for surface in surfaces],
|
|
)
|
|
self.assertEqual(
|
|
"example.module",
|
|
next(
|
|
surface.parent_id
|
|
for surface in surfaces
|
|
if surface.id == "example.route.example"
|
|
),
|
|
)
|
|
|
|
def test_registry_includes_valid_custom_surface(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="example.settings.connection",
|
|
module_id="example",
|
|
kind="section",
|
|
label="Connection settings",
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
registry.validate()
|
|
|
|
custom_surface = next(
|
|
surface
|
|
for surface in registry.view_surfaces()
|
|
if surface.id == "example.settings.connection"
|
|
)
|
|
self.assertEqual("example.module", custom_surface.parent_id)
|
|
|
|
def test_registry_rejects_surface_owned_by_another_module(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="other.settings.connection",
|
|
module_id="other",
|
|
kind="section",
|
|
label="Connection settings",
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
with self.assertRaisesRegex(RegistryError, "belongs to"):
|
|
registry.validate()
|
|
|
|
def test_registry_rejects_unknown_surface_parent(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="example.settings.connection",
|
|
module_id="example",
|
|
kind="section",
|
|
label="Connection settings",
|
|
parent_id="example.missing",
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
with self.assertRaisesRegex(RegistryError, "unknown parent"):
|
|
registry.validate()
|
|
|
|
def test_registry_rejects_cyclic_surface_hierarchy(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="example.section.one",
|
|
module_id="example",
|
|
kind="section",
|
|
label="One",
|
|
parent_id="example.section.two",
|
|
),
|
|
ViewSurface(
|
|
id="example.section.two",
|
|
module_id="example",
|
|
kind="section",
|
|
label="Two",
|
|
parent_id="example.section.one",
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
with self.assertRaisesRegex(RegistryError, "contains a cycle"):
|
|
registry.validate()
|
|
|
|
def test_required_surface_must_be_visible_by_default(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="example.recovery",
|
|
module_id="example",
|
|
kind="action",
|
|
label="Recovery",
|
|
required=True,
|
|
default_visible=False,
|
|
),
|
|
)
|
|
)
|
|
)
|
|
|
|
with self.assertRaisesRegex(RegistryError, "visible by default"):
|
|
registry.validate()
|
|
|
|
def test_registry_validates_automatically_derived_surface_ids(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
ModuleManifest(
|
|
id="example",
|
|
name="Example",
|
|
version="test",
|
|
frontend=FrontendModule(
|
|
module_id="example",
|
|
routes=(
|
|
FrontendRoute(
|
|
path=f"/{'segment-' * 30}",
|
|
component="LongRoutePage",
|
|
),
|
|
),
|
|
),
|
|
)
|
|
)
|
|
|
|
with self.assertRaisesRegex(RegistryError, "View surface id"):
|
|
registry.validate()
|
|
|
|
def test_platform_module_payload_exposes_normalized_surface_contract(self) -> None:
|
|
registry = PlatformRegistry()
|
|
registry.register(
|
|
example_manifest(
|
|
custom_surfaces=(
|
|
ViewSurface(
|
|
id="example.section.details",
|
|
module_id="example",
|
|
kind="section",
|
|
label="Details",
|
|
),
|
|
)
|
|
)
|
|
)
|
|
registry.validate()
|
|
app = FastAPI()
|
|
app.state.govoplan_registry = registry
|
|
app.include_router(create_platform_router(), prefix="/api/v1")
|
|
app.dependency_overrides[get_api_principal] = lambda: object()
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/v1/platform/modules")
|
|
|
|
self.assertEqual(200, response.status_code)
|
|
frontend = response.json()["modules"][0]["frontend"]
|
|
self.assertEqual("1", frontend["view_surface_contract_version"])
|
|
self.assertEqual(
|
|
"example.route.example",
|
|
frontend["routes"][0]["surface_id"],
|
|
)
|
|
surfaces = {
|
|
surface["id"]: surface
|
|
for surface in frontend["view_surfaces"]
|
|
}
|
|
self.assertEqual(
|
|
"example.module",
|
|
surfaces["example.section.details"]["parent_id"],
|
|
)
|
|
|
|
def test_access_admin_route_accepts_view_manager_permissions(self) -> None:
|
|
access = available_module_manifests()["access"]
|
|
admin_route = next(
|
|
route
|
|
for route in access.frontend.routes
|
|
if route.path == "/admin"
|
|
)
|
|
|
|
self.assertTrue(
|
|
{
|
|
"views:definition:read",
|
|
"views:assignment:read",
|
|
"views:system_definition:read",
|
|
"views:system_assignment:read",
|
|
}.issubset(admin_route.required_any)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|