Move the pro mount decnet/services/pro/ -> decnet/pro/ so the Professional tier
can contribute to more than honeypots. The core wires each surface only when
decnet/pro/ is present (absence stays the entitlement gate):
* services — registry scans decnet/pro/services/ (was decnet/services/pro/)
* API routes — decnet/pro/routes.py exposes ROUTERS, mounted under /api/v1
* web pages — Vite aliases @pro to the pro frontend (community -> empty stub),
App.tsx maps proRoutes into <Route>s, Layout renders a
PROFESSIONAL nav group; both tree-shake out of the community build
Frontend gate mirrors the existing VITE_DECNET_DEVELOPER tree-shake pattern.
Tests: registry + router seams (backend), empty-stub contract (frontend).
89 lines
3.1 KiB
Python
89 lines
3.1 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Open-core tier seams: the Professional tier is a separate private repo mounted at
|
|
decnet/pro/ (git-ignored here) that contributes to several core surfaces, each
|
|
wired only when the package is present:
|
|
|
|
* decnet/pro/services/ — advanced honeypots, discovered by the service registry.
|
|
* decnet/pro/routes.py — ROUTERS, mounted under /api/v1 by the web router.
|
|
|
|
Absence of decnet/pro/ is the entitlement gate; there is no licence check.
|
|
|
|
One test function on purpose: it mutates decnet/pro/services/, the process-global
|
|
service registry, and reloads decnet.web.router — doing that sequentially in one
|
|
worker avoids the xdist races that separate tests on shared state would hit.
|
|
"""
|
|
|
|
import gc
|
|
import importlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import decnet.services.registry as reg
|
|
|
|
_DEMO_MOD = "_demo_pro_tier_test"
|
|
_DEMO_NAME = "demo-pro-honeypot"
|
|
|
|
|
|
def _reload_registry():
|
|
reg._loaded = False
|
|
reg._registry.clear()
|
|
reg._load_plugins()
|
|
|
|
|
|
def test_pro_tier_seams():
|
|
# --- service-discovery seam -------------------------------------------
|
|
services_dir = Path(reg.__file__).parent.parent / "pro" / "services"
|
|
demo = services_dir / f"{_DEMO_MOD}.py"
|
|
created_dir = not services_dir.exists()
|
|
if created_dir:
|
|
services_dir.mkdir(parents=True)
|
|
(services_dir / "__init__.py").write_text("")
|
|
|
|
try:
|
|
_reload_registry()
|
|
assert _DEMO_NAME not in reg.all_services() # gate closed
|
|
assert "ssh" in reg.all_services() # community untouched
|
|
|
|
# A pro honeypot that EXTENDS a community service — only reachable via
|
|
# the registry's recursive subclass walk.
|
|
demo.write_text(
|
|
"from decnet.services.ssh import SSHService\n"
|
|
"class DemoProHoneypot(SSHService):\n"
|
|
f" name = {_DEMO_NAME!r}\n"
|
|
)
|
|
_reload_registry()
|
|
assert _DEMO_NAME in reg.all_services()
|
|
finally:
|
|
demo.unlink(missing_ok=True)
|
|
if created_dir:
|
|
import shutil
|
|
shutil.rmtree(services_dir)
|
|
sys.modules.pop(f"decnet.pro.services.{_DEMO_MOD}", None)
|
|
gc.collect()
|
|
_reload_registry()
|
|
|
|
# --- API-router seam --------------------------------------------------
|
|
import decnet.pro.routes as pro_routes
|
|
import decnet.web.router as web_router
|
|
from fastapi import APIRouter, FastAPI
|
|
|
|
saved = pro_routes.ROUTERS
|
|
probe = APIRouter()
|
|
|
|
@probe.get("/pro/_probe_test")
|
|
async def _probe(): # pragma: no cover - never called, just registered
|
|
return {}
|
|
|
|
try:
|
|
pro_routes.ROUTERS = [probe]
|
|
importlib.reload(web_router)
|
|
# This FastAPI version defers route flattening (_IncludedRouter), so go
|
|
# through openapi() — it forces resolution and lists effective paths.
|
|
app = FastAPI()
|
|
app.include_router(web_router.api_router, prefix="/api/v1")
|
|
assert "/api/v1/pro/_probe_test" in app.openapi()["paths"]
|
|
finally:
|
|
pro_routes.ROUTERS = saved
|
|
importlib.reload(web_router) # rebuild a pro-free api_router for others
|