Files
cms-SiteForge/tests/test_p0_p4_completion_v89.py
T
浅念 8cea7dee20 chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests)
- Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR
  imports keep post/product types instead of collapsing to page
- Docs: trade-pro theme port spec (R3) and implementation plan

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 16:49:25 +08:00

135 lines
6.9 KiB
Python

"""Deterministic P0/P2 regression fixtures; no outside network or Docker dependency."""
import io
import json
import sqlite3
import zipfile
from pathlib import Path
import pytest
from app import db, backup, wp_migration_v82 as media
from scripts.restore_backup import restore
def test_responsive_media_preserves_sizes_and_catches_cdn():
original={
'cover':'https://example.org/cover.jpg',
'body':'''<picture><source srcset="https://example.org/small.jpg 360w, https://example.org/big.jpg 1440w">
<img src="https://example.org/small.jpg" data-src="https://example.org/full.jpg"
srcset="https://example.org/small.jpg 360w, https://example.org/big.jpg 1440w"
data-srcset="https://cdn.example.net/cdn.jpg 2x"></picture>'''
}
targets=media._media_urls(original)
assert len(targets)==5 and 'https://example.org/big.jpg' in targets
assert 'https://cdn.example.net/cdn.jpg' in targets
converted={x:'/media/'+x.rsplit('/',1)[-1] for x in targets if 'example.org/' in x}
out,changed=media._rewrite_media_html(original['body'],converted)
assert changed
assert '/media/big.jpg 1440w' in out
assert '/media/small.jpg 360w' in out
assert 'data-src="/media/full.jpg"' in out
assert 'https://cdn.example.net/cdn.jpg 2x' in out
assert media._media_urls({'body':out})==['https://cdn.example.net/cdn.jpg']
def test_restore_tampered_archive_cannot_change_existing_data(tmp_path):
target=tmp_path/'restore';target.mkdir()
(target/'old.txt').write_text('must survive')
for filename in ('../outside.txt','evil.txt'):
archive=tmp_path/(filename.replace('/','_')+'.zip')
with zipfile.ZipFile(archive,'w') as z:
z.writestr('siteforge.sqlite3',b'not a database')
z.writestr(filename,'evil')
with pytest.raises(ValueError):restore(archive,target,confirmed=True)
assert (target/'old.txt').read_text()=='must survive'
assert not (tmp_path/'outside.txt').exists()
def test_restore_replaces_directory_atomically_and_preserves_previous(tmp_path,monkeypatch):
source=tmp_path/'original';source.mkdir()
monkeypatch.setattr(db,'DATA',source)
monkeypatch.setattr(db,'DB_FILE',source/'siteforge.sqlite3')
db.init()
db.insert('INSERT INTO sites(id,name,created_at,updated_at) VALUES(?,?,?,?)',
('testsite','Restore Success',db.now(),db.now()))
(source/'media').mkdir();(source/'media'/'image.png').write_bytes(b'img')
archive=backup.backup()
target=tmp_path/'production';target.mkdir()
(target/'old-sentinel').write_text('old installation')
out=restore(archive,target,confirmed=True)
assert (target/'media'/'image.png').read_bytes()==b'img'
assert not (target/'old-sentinel').exists()
assert out['previous'] and (Path(out['previous'])/'old-sentinel').read_text()=='old installation'
with sqlite3.connect(target/'siteforge.sqlite3') as conn:
assert conn.execute('SELECT name FROM sites WHERE id=?',('testsite',)).fetchone()[0]=='Restore Success'
def test_remote_only_media_candidate_reported():
assert media._media_urls({'body':'<img src="/media/local.png" srcset="https://cdn.example.net/a.png 720w">'})==['https://cdn.example.net/a.png']
def test_build_quality_reports_missing_local_assets_and_blocks_traversal(tmp_path,monkeypatch):
from app import quality
monkeypatch.setattr(db,'DATA',tmp_path)
monkeypatch.setattr(db,'DB_FILE',tmp_path/'db.sqlite3')
db.init()
sid='p0p4qa';now=db.now()
db.insert('INSERT INTO sites(id,name,domain,created_at,updated_at) VALUES(?,?,?,?,?)',
(sid,'QA','https://example.org',now,now))
dist=tmp_path/'builds'/sid/'sample'/'dist'
dist.mkdir(parents=True)
(dist/'index.html').write_text('''<html><head><title>Home</title><meta name="description" content="A Home"></head>
<body><h1>Welcome</h1><a href="/missing/">Missing</a>
<img alt="OK" src="/assets/not-there.png"><a href="/../secrets">Bad</a></body></html>''')
audit=quality.report(sid,'sample')
assert audit['files']==1 and audit['errors']==3
assert {x['type'] for x in audit['issues']}=={'broken_internal_reference'}
(dist/'assets').mkdir();(dist/'assets'/'not-there.png').write_bytes(b'demo')
(dist/'missing').mkdir();(dist/'missing'/'index.html').write_text('<title>Missing</title><h1>There</h1>')
audit=quality.report(sid,'sample')
assert audit['errors']==1 # the traversal never resolves to an outside file
def test_catalogue_prepared_context_reuses_groups_without_mutating_records():
from app import theme_pro
rows=[{'type':'vehicle','title':f'Model {i}','route':f'/vehicle/{i}/',
'meta_json':json.dumps({'fields':{'brand':'Acme','category':'SUV'}})} for i in range(30)]
prepared=theme_pro.prepare(rows)
assert prepared['brands']==[{'name':'Acme','url':'/brands/acme/'}]
assert prepared['categories']==[{'name':'SUV','url':'/categories/suv/'}]
site={'name':'Demo','theme':'automotive','domain':'','settings_json':'{}','tagline':''}
ctx=theme_pro.make_context(site,rows[0],rows,prepared=prepared)
assert ctx['kind']=='product' and ctx['listing']==[]
assert ctx['groups'] is prepared['groups']
assert rows[0]['title']=='Model 0'
def test_static_pagination_keeps_every_product_reachable(tmp_path,monkeypatch):
from app import builder,quality,v8_workflows as wf
from app.main import app
from app.core import uid
from fastapi.testclient import TestClient
monkeypatch.setattr(db,'DATA',tmp_path)
monkeypatch.setattr(db,'DB_FILE',tmp_path/'pagination.sqlite3')
monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','pagination-demo-only-password-long')
monkeypatch.setenv('SITEFORGE_SESSION_SECRET','pagination-demo-only-secret-long-enough-1234')
with TestClient(app):
sid=uid();now=db.now()
db.insert('INSERT INTO sites(id,name,theme,created_at,updated_at) VALUES(?,?,?,?,?)',
(sid,'Pagination Pilot','automotive',now,now))
entries=[{'车型名称':f'Product {i:04d}','品牌':'Maker','分类':'SUV'} for i in range(235)]
b=wf.stage(sid,entries,kind='vehicle')
assert wf.commit(sid,b['batch_id'])['created']==235
result=builder.build(sid,False)
dist=tmp_path/'builds'/sid/result['version']/'dist'
first=(dist/'categories/suv/index.html').read_text()
second=(dist/'categories/suv/page/2/index.html').read_text()
third=(dist/'categories/suv/page/3/index.html').read_text()
assert '/categories/suv/page/2/' in first
assert '/categories/suv/page/3/' in second
assert '/categories/suv/page/2/' in third
assert first.count('pro-card pro-card-catalog')==100
assert second.count('pro-card pro-card-catalog')==100
assert third.count('pro-card pro-card-catalog')==35
assert (dist/'vehicle/page/2/index.html').is_file()
assert (dist/'products/page/2/index.html').is_file()
assert quality.report(sid,result['version'])['errors']==0