mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 13:35:12 +09:00
The largest single move in the plan (#38). The entire WordPress backend leaves core for the bundled, default-installed serverkit-wordpress extension (D4). Moved (12 files → builtin-extensions/serverkit-wordpress/backend/, intra-extension imports made relative, core imports stay absolute): api/wordpress.py, api/wordpress_sites.py, api/environment_pipeline.py, wordpress_service.py (2676 lines), git_wordpress_service, wordpress_env_service, wordpress_plugin_library_service, wp_analytics/reports/security/update/vulnerability. Wiring: - Blueprints keep /api/v1/wordpress, /wordpress/projects and the /wordpress/pipelines alias (D9) via new manifest `extra_blueprints` support (3 blueprints / 4 mounts). - Flagship (D4): seeded active on every panel (fresh + upgrade), uninstallable, never deletes tracked builtin/pre-bundled files; the `wordpress` module toggle is retired (MODULES now empty) — the plugin status guard is the gate. - Loadability: the dashed package app.plugins.serverkit-wordpress is registered in-place via spec_from_file_location (no copy) — uniform across prod/dev/test. - The 11 WP test files re-pointed through app.services.wordpress_bridge; the 6 lazy core callers were re-pointed in the prior commit. - _regenerate_frontend_manifest() is a no-op under testing so the flagship boot seed can't clobber the checked-in plugins-manifest.json. Deviations (documented, matching the Email precedent): - WordPress *models* stay core — status_page/environment_activity/promotion_job hold FKs into wordpress_sites, so relocation would be a breaking data migration. - Event catalog + Fail2ban WP filter stay core as two-speed items (extension calls them via core APIs); inverting adds risk without blocking the move. - Frontend UI stays core this session (it lives in a TabGroupLayout — the tab-grouped conversion Phase 1 deliberately deferred); the extension is backend-only, so there is no route duplication. Frontend contribution is the scoped follow-up. Verified: clean collection across all 1223 tests; 199 passed / 2 skipped / 0 failed across the WP + plugin/extension + module + backup + core-caller parity set, incl. the new tests/test_wordpress_extraction.py proving the move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.8 KiB
Python
86 lines
3.8 KiB
Python
"""Prove the WordPress extraction (Phase 5, #38).
|
|
|
|
WordPress physically moved out of core into the bundled, default-installed
|
|
``serverkit-wordpress`` extension (a flagship, D4). Unlike a niche extension it
|
|
ships installed on every panel, so the proof is the mirror of the Email one:
|
|
|
|
* the WordPress *services* are gone from ``app.services`` (physical move);
|
|
* they load instead from the dashed package ``app.plugins.serverkit-wordpress``
|
|
via the importlib bridge — in-place, no copy (dev/test loadability);
|
|
* a stock panel has the ``/api/v1/wordpress`` blueprints registered *from the
|
|
extension* (seeded flagship), including the ``/pipelines`` alias and
|
|
``/projects`` mounts (D9);
|
|
* uninstall records the user's choice and NEVER deletes the tracked builtin /
|
|
pre-bundled files.
|
|
"""
|
|
import importlib
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from app.models.plugin import InstalledPlugin
|
|
from app.services import plugin_service, wordpress_bridge
|
|
|
|
SLUG = 'serverkit-wordpress'
|
|
|
|
|
|
def test_wordpress_services_left_core():
|
|
"""The WordPress service files no longer exist under app.services."""
|
|
for mod in ('wordpress_service', 'git_wordpress_service',
|
|
'wordpress_env_service', 'wp_update_service'):
|
|
with pytest.raises(ModuleNotFoundError):
|
|
importlib.import_module(f'app.services.{mod}')
|
|
|
|
|
|
def test_core_services_pkg_drops_wordpress_export():
|
|
import app.services as services_pkg
|
|
assert 'WordPressService' not in getattr(services_pkg, '__all__', [])
|
|
assert not hasattr(services_pkg, 'WordPressService')
|
|
|
|
|
|
def test_bridge_resolves_from_extension_in_place():
|
|
"""The bridge loads WordPressService from the dashed extension package,
|
|
without the plugin being copy-installed under app/plugins/."""
|
|
WordPressService = wordpress_bridge.wordpress_service()
|
|
assert WordPressService.__name__ == 'WordPressService'
|
|
assert WordPressService.__module__.startswith(f'app.plugins.{SLUG}')
|
|
|
|
|
|
def test_wordpress_seeded_as_flagship(app):
|
|
"""A stock panel ships WordPress installed-by-default (D4)."""
|
|
plugin = InstalledPlugin.query.filter_by(slug=SLUG).first()
|
|
assert plugin is not None, 'WordPress flagship should be seeded on boot'
|
|
assert plugin.status == InstalledPlugin.STATUS_ACTIVE
|
|
assert plugin.has_backend is True
|
|
assert plugin.url_prefix == '/api/v1/wordpress'
|
|
|
|
|
|
def test_wordpress_blueprints_registered_from_extension(app):
|
|
rules = [r.rule for r in app.url_map.iter_rules()]
|
|
wp_rules = [r for r in rules if r.startswith('/api/v1/wordpress')]
|
|
assert wp_rules, 'WordPress API should be registered from the extension'
|
|
# D9: the /projects and /pipelines (alias) mounts both survive the move.
|
|
assert any(r.startswith('/api/v1/wordpress/projects') for r in wp_rules)
|
|
assert any(r.startswith('/api/v1/wordpress/pipelines') for r in wp_rules)
|
|
|
|
|
|
def test_wordpress_api_responds_not_404_or_503(app, client, auth_headers):
|
|
"""The extension's blueprint is live: /sites is wired (not 404) and its
|
|
status guard passes for the active flagship (not 503)."""
|
|
resp = client.get('/api/v1/wordpress/sites', headers=auth_headers)
|
|
assert resp.status_code not in (404, 503), resp.status_code
|
|
|
|
|
|
def test_uninstall_flagship_keeps_tracked_files_and_marks(app):
|
|
plugin = InstalledPlugin.query.filter_by(slug=SLUG).first()
|
|
assert plugin is not None
|
|
builtin_init = os.path.join(
|
|
plugin_service.BUILTIN_EXTENSIONS_DIR, SLUG, 'backend', '__init__.py')
|
|
assert os.path.isfile(builtin_init)
|
|
|
|
assert plugin_service.uninstall_plugin(plugin.id) is True
|
|
# Row gone, marker set, and the tracked builtin source is untouched.
|
|
assert InstalledPlugin.query.filter_by(slug=SLUG).first() is None
|
|
assert SLUG in plugin_service._flagship_uninstalled_set()
|
|
assert os.path.isfile(builtin_init), 'uninstall must not delete builtin files'
|