feat(core): support explicit public module routes
This commit is contained in:
@@ -30,7 +30,7 @@ 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 import APIRouter
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
@@ -99,7 +99,7 @@ from govoplan_core.core.module_package_catalog import (
|
||||
sign_module_package_catalog,
|
||||
validate_module_package_catalog,
|
||||
)
|
||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult
|
||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult, PublicFrontendRoute
|
||||
from govoplan_core.core.module_guards import drop_table_retirement_provider
|
||||
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest, RoleTemplate
|
||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||
@@ -112,6 +112,7 @@ from govoplan_core.security.module_permissions import scopes_grant_compatible
|
||||
from govoplan_core.security.permissions import scope_grants
|
||||
from govoplan_core.server.app import create_app
|
||||
from govoplan_core.server.config import GovoplanServerConfig
|
||||
from govoplan_core.server.platform import create_platform_router
|
||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||
from govoplan_core.server.route_validation import RouteCollisionError
|
||||
from govoplan_core.tenancy.scope import Tenant, create_scope_tables
|
||||
@@ -338,7 +339,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertEqual(manifest.id, manifest.frontend.module_id)
|
||||
if manifest.frontend.package_name is not None:
|
||||
self.assertRegex(manifest.frontend.package_name, _PACKAGE_NAME_RE)
|
||||
for route in (*manifest.frontend.routes, *manifest.frontend.settings_routes):
|
||||
for route in (*manifest.frontend.routes, *manifest.frontend.settings_routes, *manifest.frontend.public_routes):
|
||||
self.assertTrue(route.path.startswith("/"))
|
||||
self.assertTrue(route.component.strip())
|
||||
for item in (*manifest.nav_items, *manifest.frontend.nav_items):
|
||||
@@ -457,6 +458,74 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RegistryError, "Frontend route"):
|
||||
registry.validate()
|
||||
|
||||
def test_public_frontend_routes_are_explicitly_allowlisted(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
registry.register(ModuleManifest(
|
||||
id="example",
|
||||
name="Example",
|
||||
version="test",
|
||||
frontend=FrontendModule(
|
||||
module_id="example",
|
||||
package_name="@govoplan/example-webui",
|
||||
routes=(FrontendRoute(path="/example", component="ExamplePage"),),
|
||||
public_routes=(
|
||||
PublicFrontendRoute(
|
||||
path="/example/public/:token",
|
||||
component="ExamplePublicPage",
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
registry.register(ModuleManifest(
|
||||
id="private",
|
||||
name="Private",
|
||||
version="test",
|
||||
frontend=FrontendModule(
|
||||
module_id="private",
|
||||
package_name="@govoplan/private-webui",
|
||||
routes=(FrontendRoute(path="/private", component="PrivatePage"),),
|
||||
),
|
||||
))
|
||||
registry.validate()
|
||||
|
||||
app = FastAPI()
|
||||
app.state.govoplan_registry = registry
|
||||
app.include_router(create_platform_router(), prefix="/api/v1")
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/api/v1/platform/public-modules")
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.text)
|
||||
self.assertEqual(["example"], [item["id"] for item in response.json()["modules"]])
|
||||
public_module = response.json()["modules"][0]
|
||||
self.assertNotIn("dependencies", public_module)
|
||||
self.assertNotIn("nav", public_module["frontend"])
|
||||
self.assertNotIn("routes", public_module["frontend"])
|
||||
self.assertEqual(
|
||||
["/example/public/:token"],
|
||||
[item["path"] for item in public_module["frontend"]["public_routes"]],
|
||||
)
|
||||
|
||||
def test_registry_rejects_duplicate_public_frontend_routes(self) -> None:
|
||||
registry = PlatformRegistry()
|
||||
for module_id in ("first", "second"):
|
||||
registry.register(ModuleManifest(
|
||||
id=module_id,
|
||||
name=module_id.title(),
|
||||
version="test",
|
||||
frontend=FrontendModule(
|
||||
module_id=module_id,
|
||||
public_routes=(
|
||||
PublicFrontendRoute(
|
||||
path="/shared/public/:token",
|
||||
component=f"{module_id.title()}PublicPage",
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
with self.assertRaisesRegex(RegistryError, "Duplicate public frontend route"):
|
||||
registry.validate()
|
||||
|
||||
def test_server_startup_rejects_duplicate_configured_routes(self) -> None:
|
||||
first = APIRouter()
|
||||
second = APIRouter()
|
||||
|
||||
Reference in New Issue
Block a user