Files
DECNET/tests/services/test_pro_tier.py
anti a47f99c449 feat(pro): generalize pro tier to multi-surface extension points
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).
2026-06-17 15:02:28 -04:00

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