mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 02:26:43 +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>
108 lines
5.0 KiB
Python
108 lines
5.0 KiB
Python
"""Tests for #24 per-site PHP limit writes — validation + durable conf.d ini /
|
|
bind-mount. The Docker apply step is mocked (no Docker on the dev/CI host); the
|
|
file-rendering, compose-mutation, merge, and injection-rejection logic is real."""
|
|
import os
|
|
import yaml
|
|
|
|
from app.services import wordpress_bridge
|
|
|
|
|
|
def _compose_site(tmp_path):
|
|
"""A minimal Docker-stack WP site dir with a docker-compose.yml."""
|
|
(tmp_path / 'docker-compose.yml').write_text(
|
|
"services:\n"
|
|
" wordpress:\n"
|
|
" image: wordpress:6.4-apache\n"
|
|
" volumes:\n"
|
|
" - wordpress_html:/var/www/html\n"
|
|
)
|
|
return str(tmp_path)
|
|
|
|
|
|
def _mock_docker(monkeypatch):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
from app.services.docker_service import DockerService
|
|
monkeypatch.setattr(DockerService, 'compose_up', lambda *a, **k: {'success': True})
|
|
monkeypatch.setattr(DockerService, 'compose_restart', lambda *a, **k: {'success': True})
|
|
monkeypatch.setattr(WordPressService, '_wait_for_wp_ready', staticmethod(lambda p: None))
|
|
monkeypatch.setattr(WordPressService, 'get_php_info', staticmethod(lambda p: {'limits': {}}))
|
|
|
|
|
|
# ---- validation / injection rejection (no Docker needed) -------------------
|
|
|
|
def test_rejects_unknown_directive(tmp_path):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
res = WordPressService.set_php_limits(_compose_site(tmp_path), {'evil_directive': '1'})
|
|
assert res['success'] is False and 'Unknown PHP limit' in res['error']
|
|
|
|
|
|
def test_rejects_injection_value(tmp_path):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
# A newline-smuggled extra directive must not pass validation.
|
|
res = WordPressService.set_php_limits(_compose_site(tmp_path),
|
|
{'memory_limit': '256M\nevil = 1'})
|
|
assert res['success'] is False and 'Invalid value' in res['error']
|
|
# ...and nothing was written.
|
|
assert not os.path.exists(os.path.join(_compose_site(tmp_path), 'php-conf', 'zz-serverkit.ini'))
|
|
|
|
|
|
def test_rejects_non_stack_site(tmp_path):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
res = WordPressService.set_php_limits(str(tmp_path), {'memory_limit': '256M'})
|
|
assert res['success'] is False and 'Not a Docker-stack site' in res['error']
|
|
|
|
|
|
# ---- durable write + bind-mount (Docker mocked) ----------------------------
|
|
|
|
def test_writes_ini_and_injects_single_file_mount(tmp_path, monkeypatch):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
_mock_docker(monkeypatch)
|
|
path = _compose_site(tmp_path)
|
|
|
|
res = WordPressService.set_php_limits(path, {'memory_limit': '512M', 'max_execution_time': '120'})
|
|
assert res['success'] is True
|
|
|
|
ini = os.path.join(path, 'php-conf', 'zz-serverkit.ini')
|
|
content = open(ini).read()
|
|
assert 'memory_limit = 512M' in content
|
|
assert 'max_execution_time = 120' in content
|
|
|
|
vols = yaml.safe_load(open(os.path.join(path, 'docker-compose.yml')))['services']['wordpress']['volumes']
|
|
assert any('php-conf/zz-serverkit.ini:/usr/local/etc/php/conf.d/zz-serverkit.ini' in v for v in vols)
|
|
|
|
|
|
def test_partial_update_merges_and_mount_not_duplicated(tmp_path, monkeypatch):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
_mock_docker(monkeypatch)
|
|
path = _compose_site(tmp_path)
|
|
|
|
WordPressService.set_php_limits(path, {'memory_limit': '256M'})
|
|
res = WordPressService.set_php_limits(path, {'upload_max_filesize': '64M'})
|
|
assert res['success'] is True
|
|
# The second (partial) update keeps the first directive.
|
|
assert res['limits']['memory_limit'] == '256M'
|
|
assert res['limits']['upload_max_filesize'] == '64M'
|
|
|
|
# The single-file mount is added once, not duplicated on the second call.
|
|
vols = yaml.safe_load(open(os.path.join(path, 'docker-compose.yml')))['services']['wordpress']['volumes']
|
|
assert len([v for v in vols if 'zz-serverkit.ini' in v]) == 1
|
|
|
|
|
|
def test_accepts_unlimited_and_size_suffixes(tmp_path, monkeypatch):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
_mock_docker(monkeypatch)
|
|
path = _compose_site(tmp_path)
|
|
res = WordPressService.set_php_limits(path, {'memory_limit': '-1', 'max_input_time': '-1',
|
|
'upload_max_filesize': '2G', 'max_input_vars': '5000'})
|
|
assert res['success'] is True
|
|
assert res['limits']['memory_limit'] == '-1'
|
|
|
|
|
|
# ---- ini parsing -----------------------------------------------------------
|
|
|
|
def test_read_php_ini_skips_comments_and_garbage(tmp_path):
|
|
WordPressService = wordpress_bridge.get('wordpress_service', 'WordPressService')
|
|
p = tmp_path / 'x.ini'
|
|
p.write_text('; a comment\nmemory_limit = 128M\n\nno equals here\nmax_input_vars = 2000\n')
|
|
assert WordPressService._read_php_ini(str(p)) == {'memory_limit': '128M', 'max_input_vars': '2000'}
|