Files
cms-SiteForge/tests/test_siteforge_v817_audit.py
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.

New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.

compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).

Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 12:31:03 +08:00

174 lines
7.6 KiB
Python

import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
MODULE_PATH = Path(__file__).resolve().parents[1] / 'scripts' / 'siteforge_v817_audit.py'
spec = importlib.util.spec_from_file_location('siteforge_audit', MODULE_PATH)
a = importlib.util.module_from_spec(spec)
import sys
sys.modules[spec.name] = a
spec.loader.exec_module(a)
class DummyArgs: pass
class AuditTests(unittest.TestCase):
def setUp(self):
self.r = a.Report(DummyArgs())
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
def tearDown(self):
self.temp.cleanup()
def statuses(self):
return {c.code: c.status for c in self.r.checks}
def test_loopback_url(self):
self.assertTrue(a._local_host('http://127.0.0.1:8787'))
self.assertTrue(a._local_host('http://[::1]:8787'))
self.assertTrue(a._local_host('https://localhost'))
def test_remote_is_not_loopback(self):
for host in ('example.com', '192.168.1.1', '169.254.169.254', '10.0.0.3'):
self.assertFalse(a._local_host('http://' + host))
def test_credentials_in_url_forbidden(self):
self.assertFalse(a._local_host('http://admin:secret@localhost:8787'))
def test_http_remote_refused_without_consent(self):
with patch.object(a, '_probe', side_effect=AssertionError('should not send a request')):
a.audit_http(self.r, 'https://example.com', False)
self.assertEqual(self.statuses()['http.scope'], 'fail')
def test_http_health_pass(self):
with patch.object(a, '_probe', side_effect=[(200, '{"status":"ok"}', ''), (200, '{"status":"ok"}', ''), (200, 'OK', '')]):
a.audit_http(self.r, 'http://127.0.0.1:8787', False)
self.assertEqual(self.statuses()['http.ready'], 'pass')
def test_http_not_ready_fails(self):
with patch.object(a, '_probe', side_effect=[(200, '{"status":"ok"}', ''), (503, '', 'bad'), (200, 'OK', '')]):
a.audit_http(self.r, 'http://127.0.0.1:8787', False)
self.assertEqual(self.statuses()['http.ready'], 'fail')
def test_http_unavailable_status_fails(self):
with patch.object(a, '_probe', side_effect=[(200, '{"status":"ok"}', ''), (200, '{"status":"unavailable"}', ''), (200, 'OK', '')]):
a.audit_http(self.r, 'http://127.0.0.1:8787', False)
self.assertEqual(self.statuses()['http.ready'], 'fail')
def test_route_with_path_traversal_rejected(self):
self.assertIsNone(a.normalized_route('/../private'))
self.assertIsNone(a.normalized_route('/%2e%2e/secret'))
self.assertIsNone(a.normalized_route('/bad\\path'))
self.assertIsNone(a.normalized_route('/a//b'))
def test_route_query_disallowed(self):
self.assertIsNone(a.normalized_route('/foo/?hello=1'))
self.assertIsNone(a.normalized_route('https://example.com/'))
self.assertEqual(a.normalized_route('/product/'), '/product/')
def test_static_html_pass(self):
(self.root / 'index.html').write_text('<html lang="en"><head><link rel="canonical" href="https://example.com/"></head><body><a href="/">Home</a></body></html>')
(self.root / 'sitemap.xml').write_text('ok')
(self.root / 'robots.txt').write_text('ok')
a.audit_build(self.r, self.root, None)
self.assertEqual(self.statuses()['build.internal_links'], 'pass')
self.assertEqual(self.statuses()['build.canonical_scheme'], 'pass')
def test_broken_internal_link_fails(self):
(self.root / 'index.html').write_text('<html lang="en"><a href="/missing/">Missing</a></html>')
a.audit_build(self.r, self.root, None)
self.assertEqual(self.statuses()['build.internal_links'], 'fail')
def test_duplicate_canonicals_fail(self):
(self.root / 'index.html').write_text('<html lang="zh"><link rel="canonical" href="https://e.com/"><link rel="canonical" href="https://e.com/2"></html>')
a.audit_build(self.r, self.root, None)
self.assertEqual(self.statuses()['build.canonical_duplicate'], 'fail')
def test_url_baseline_coverage(self):
(self.root / 'index.html').write_text('<html lang="en"></html>')
b = self.root / 'urls.txt'
b.write_text('/\n/product/\n')
a.audit_build(self.r, self.root, b)
self.assertEqual(self.statuses()['build.url_baseline'], 'fail')
(self.root / 'product').mkdir()
(self.root / 'product' / 'index.html').write_text('<html lang="en"></html>')
r2 = a.Report(DummyArgs())
a.audit_build(r2, self.root, b)
self.assertEqual({x.code:x.status for x in r2.checks}['build.url_baseline'], 'pass')
def test_invalid_baseline_route_fails(self):
(self.root / 'index.html').write_text('<html lang="en"></html>')
b = self.root / 'urls.txt'
b.write_text('/\n/../secret\n')
a.audit_build(self.r, self.root, b)
self.assertEqual(self.statuses()['build.url_baseline'], 'fail')
def test_html_reports_escape_untrusted_text(self):
self.r.add('test', 'http', 'warn', '<script>alert(1)</script>', '<img src=x onerror=alert(2)>')
x = a.html_report(self.r.data())
self.assertNotIn('<script>alert(1)</script>', x)
self.assertNotIn('<img src=x onerror=alert(2)>', x)
self.assertIn('&lt;script&gt;', x)
def test_report_ready_requires_all_pass(self):
self.r.add('test', 'http', 'pass', 'ok')
self.assertTrue(self.r.data()['selected_checks_clear'])
self.assertFalse(self.r.data()['production_ready'])
self.r.add('skip', 'release', 'skip', 'not done')
self.assertFalse(self.r.data()['selected_checks_clear'])
def test_release_evidence_requires_proof_fields(self):
f = self.root/'evidence.json'
f.write_text('{"isolated_pg":{"status":"passed","operator":"team"}}')
a.release_gate(self.r, f)
self.assertEqual(self.statuses()['release.isolated_pg'], 'warn')
self.assertEqual(len(self.r.checks), 10)
def test_release_no_evidence_not_passed(self):
a.release_gate(self.r, None)
self.assertEqual(self.statuses()['release.evidence'], 'skip')
def test_repo_minimal_fails_softly(self):
(self.root/'app').mkdir()
(self.root/'app'/'test.py').write_text('x = 1')
a.audit_repo(self.r, self.root)
self.assertEqual(self.statuses()['repo.python_syntax'], 'pass')
self.assertEqual(self.statuses()['repo.app.builder.py'], 'warn')
def test_repo_python_syntax_fail(self):
(self.root/'app').mkdir()
(self.root/'app'/'bad.py').write_text('def oops(\n')
a.audit_repo(self.r, self.root)
self.assertEqual(self.statuses()['repo.python_syntax'], 'fail')
def test_reports_generated_cli(self):
out = self.root/'audit'
f = self.root/'build'
f.mkdir()
(f/'index.html').write_text('<html lang="zh-CN"></html>')
code = a.main(['--build-dir', str(f), '--output', str(out)])
self.assertEqual(code, 0)
for ext in ('.json', '.md', '.html'):
self.assertTrue(out.with_suffix(ext).exists())
self.assertEqual(json.loads(out.with_suffix('.json').read_text())['live_production_verified'], False)
def test_no_redis_settings_skipped(self):
with patch.dict(os.environ, {'SITEFORGE_REDIS_URL':''}):
a.audit_redis(self.r)
self.assertEqual(self.statuses()['redis.config'], 'skip')
def test_no_pg_settings_skipped(self):
with patch.dict(os.environ, {'SITEFORGE_DATABASE_URL':''}):
a.audit_postgres(self.r)
self.assertEqual(self.statuses()['postgres.config'], 'skip')
if __name__ == '__main__':
unittest.main()