Files
ServerKit/backend/tests/test_wp_php_errors.py
T
Juan DenisandClaude Opus 4.8 733f866f1c feat(wordpress): per-site PHP-fatals analytics from the WP_DEBUG log (#25)
Finish #25's deferred PHP-fatals signal, sourced from the WP_DEBUG log
(/tmp/wp-debug.log) that #30's Security toggle writes — not the access log
(which can't carry it):

- get_php_errors: one bounded docker exec (marker-guarded tail -c, 15s single-
  worker timeout) distinguishes logging-off / no-log from on-but-clean, then
  parses [time] PHP <Level>: <msg> into severity counts (fatal/warning/notice/
  deprecated; Parse + Recoverable-fatal fold into fatal) + the 25 most-recent
  entries (messages capped, stack-trace lines skipped).
- Merged into GET /sites/<id>/analytics as php_errors; a PHP errors panel added
  to the Analytics tab with an 'enable WP_DEBUG on the Security tab' empty state.
- Supplies the per-site error signal #27's error-spike rule wanted.

7 new tests (classification, parse/skip-traces, off/empty/unavailable, capping);
full backend suite 218 passed / 12 skipped; frontend lint-clean + production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:06:58 -04:00

88 lines
3.7 KiB
Python

"""Tests for #25 PHP-fatals ingest — parsing the WP_DEBUG log (docker exec mocked)."""
_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):
import app.services.wp_analytics_service as mod
monkeypatch.setattr(mod.subprocess, 'run', lambda *a, **k: _Res(rc, out))
def test_classify_php_levels():
from app.services.wp_analytics_service import WpAnalyticsService as W
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):
from app.services.wp_analytics_service import 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):
from app.services.wp_analytics_service import 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):
from app.services.wp_analytics_service import 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):
from app.services.wp_analytics_service import 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():
from app.services.wp_analytics_service import WpAnalyticsService
out = WpAnalyticsService.get_php_errors(None)
assert out['available'] is False and out['total'] == 0
def test_message_truncation(monkeypatch):
from app.services.wp_analytics_service import 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