mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 16:47:01 +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>
89 lines
3.8 KiB
Python
89 lines
3.8 KiB
Python
"""Tests for #25 PHP-fatals ingest — parsing the WP_DEBUG log (docker exec mocked)."""
|
|
from app.services import wordpress_bridge
|
|
|
|
_SAMPLE = (
|
|
'__SK_WPDEBUG__\n'
|
|
'[15-Jun-2026 12:00:01 UTC] PHP Fatal error: Uncaught Error: Call to undefined '
|
|
'function foo() in /var/www/html/wp-content/plugins/x/x.php:12\n'
|
|
'Stack trace:\n'
|
|
'#0 /var/www/html/index.php(17): require()\n'
|
|
'#1 {main}\n'
|
|
' thrown in /var/www/html/wp-content/plugins/x/x.php on line 12\n'
|
|
'[15-Jun-2026 12:05:00 UTC] PHP Warning: Undefined array key "k" in /t/functions.php on line 5\n'
|
|
'[15-Jun-2026 12:06:00 UTC] PHP Deprecated: Function xyz() is deprecated in /x on line 1\n'
|
|
'[15-Jun-2026 12:07:00 UTC] PHP Notice: Undefined variable $z in /x on line 2\n'
|
|
)
|
|
|
|
|
|
class _Res:
|
|
def __init__(self, rc, out):
|
|
self.returncode = rc
|
|
self.stdout = out
|
|
self.stderr = ''
|
|
|
|
|
|
def _mock_run(monkeypatch, rc, out):
|
|
mod = wordpress_bridge.load('wp_analytics_service')
|
|
monkeypatch.setattr(mod.subprocess, 'run', lambda *a, **k: _Res(rc, out))
|
|
|
|
|
|
def test_classify_php_levels():
|
|
W = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
assert W._classify_php('Fatal error') == 'fatal'
|
|
assert W._classify_php('Parse error') == 'fatal'
|
|
assert W._classify_php('Recoverable fatal error') == 'fatal'
|
|
assert W._classify_php('Warning') == 'warning'
|
|
assert W._classify_php('Deprecated') == 'deprecated'
|
|
assert W._classify_php('Notice') == 'notice'
|
|
assert W._classify_php('Strict standards') == 'other'
|
|
|
|
|
|
def test_get_php_errors_parses_and_skips_stack_traces(monkeypatch):
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
_mock_run(monkeypatch, 0, _SAMPLE)
|
|
out = WpAnalyticsService.get_php_errors('site')
|
|
assert out['available'] is True and out['enabled'] is True
|
|
assert out['total'] == 4 # stack-trace / "thrown in" lines are skipped
|
|
assert out['counts'] == {'fatal': 1, 'warning': 1, 'notice': 1, 'deprecated': 1, 'other': 0}
|
|
# Most-recent first: the Notice (12:07) leads.
|
|
assert out['recent'][0]['severity'] == 'notice'
|
|
assert 'Uncaught Error' in out['recent'][-1]['message']
|
|
|
|
|
|
def test_get_php_errors_logging_off(monkeypatch):
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
_mock_run(monkeypatch, 0, '') # no marker => log file absent
|
|
out = WpAnalyticsService.get_php_errors('site')
|
|
assert out['available'] is False
|
|
assert 'logging is off' in out['note']
|
|
|
|
|
|
def test_get_php_errors_on_but_empty(monkeypatch):
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
_mock_run(monkeypatch, 0, '__SK_WPDEBUG__\n') # log exists, no entries
|
|
out = WpAnalyticsService.get_php_errors('site')
|
|
assert out['available'] is True and out['total'] == 0
|
|
assert 'no PHP errors recorded' in out['note']
|
|
|
|
|
|
def test_get_php_errors_container_unavailable(monkeypatch):
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
_mock_run(monkeypatch, 1, '') # docker exec non-zero (stopped / missing)
|
|
out = WpAnalyticsService.get_php_errors('site')
|
|
assert out['available'] is False and 'unavailable' in out['note']
|
|
|
|
|
|
def test_get_php_errors_no_container():
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
out = WpAnalyticsService.get_php_errors(None)
|
|
assert out['available'] is False and out['total'] == 0
|
|
|
|
|
|
def test_message_truncation(monkeypatch):
|
|
WpAnalyticsService = wordpress_bridge.get('wp_analytics_service', 'WpAnalyticsService')
|
|
long = 'x' * 500
|
|
_mock_run(monkeypatch, 0, f'__SK_WPDEBUG__\n[1 UTC] PHP Fatal error: {long}\n')
|
|
out = WpAnalyticsService.get_php_errors('site')
|
|
assert out['counts']['fatal'] == 1
|
|
assert len(out['recent'][0]['message']) == 300 # capped
|