8cea7dee20
- 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>
197 lines
13 KiB
Python
197 lines
13 KiB
Python
"""Regression tests for V4: theme layouts, migration audit, assembly, WXR.
|
|
No external HTTP/DNS or Docker engine required.
|
|
"""
|
|
import io,json,zipfile
|
|
from pathlib import Path
|
|
from html import escape
|
|
from openpyxl import Workbook
|
|
from app import db,blueprints,theme_store,migration_audit,wordpress_compat
|
|
from test_factory_v2 import factory,site
|
|
|
|
|
|
def test_nested_grid_validates_and_builds(factory):
|
|
sid=site(factory)
|
|
home=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/')
|
|
layout=blueprints.new_block('layout',{'columns':[
|
|
{'width':2,'blocks':[blueprints.new_block('richtext',{'title':'品牌故事','body':'<p>我们的企业介绍</p>'})]},
|
|
{'width':1,'blocks':[blueprints.new_block('cta',{'title':'欢迎合作','href':'/contact/'})]}]})
|
|
result=factory.put(f'/api/sites/{sid}/pages/{home["id"]}/blueprint',json={'blocks':[layout],
|
|
'page_template':'home','layout_mode':'replace'})
|
|
assert result.status_code==200,result.text
|
|
assert result.json()['layout_mode']=='replace'
|
|
preview=factory.post(f'/api/sites/{sid}/pages/{home["id"]}/editor-preview',json={'blocks':[layout],'page_template':'home','layout_mode':'replace'})
|
|
assert preview.status_code==200,preview.text
|
|
assert '品牌故事' in preview.text and 'sf-layout-grid' in preview.text and 'pro-header' in preview.text
|
|
version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
published=factory.get(f'/preview/{sid}/{version}/').text
|
|
assert '品牌故事' in published and 'sf-layout-grid' in published and 'pro-header' in published
|
|
css=factory.get(f'/preview/{sid}/{version}/assets/blocks.css').text
|
|
assert '@media(max-width:540px)' in css
|
|
|
|
|
|
def test_layout_rejects_duplicate_and_nested_excess(factory):
|
|
a=blueprints.new_block('richtext');b=blueprints.new_block('richtext');b['id']=a['id']
|
|
bad=blueprints.new_block('layout',{'columns':[{'width':1,'blocks':[a,b]}]})
|
|
try:blueprints.validate_blocks([bad]);assert False,'duplicate should be rejected'
|
|
except ValueError:pass
|
|
nested=blueprints.new_block('layout');child=blueprints.new_block('layout');grand=blueprints.new_block('layout')
|
|
child['columns'][0]['blocks']=[grand];nested['columns'][0]['blocks']=[child]
|
|
try:blueprints.validate_blocks([nested]);assert False,'deep nesting should be rejected'
|
|
except ValueError:pass
|
|
|
|
|
|
def test_wordpress_elementor_media_taxonomy_and_woo_gallery(factory):
|
|
sid=site(factory)
|
|
widgets=json.dumps([{'widgetType':'heading','settings':{'title':'核心价值','header_size':'h2'}},
|
|
{'widgetType':'image','settings':{'image':{'url':'https://img.example/media.jpg'}}},
|
|
{'widgetType':'unknown_plugin_widget','settings':{'value':'secret'}}],ensure_ascii=False)
|
|
wxr='''<?xml version="1.0"?><rss xmlns:wp="http://wordpress.org/export/1.2/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel>
|
|
<item><title>图片</title><wp:post_type>attachment</wp:post_type><wp:post_id>12</wp:post_id><wp:attachment_url>https://img.example/cover.jpg</wp:attachment_url></item>
|
|
<item><title>产品A</title><link>https://wp.example/product/test-a/</link><wp:post_type>product</wp:post_type><wp:status>publish</wp:status><content:encoded></content:encoded>
|
|
<category domain="product_cat">运动型多用途车</category>
|
|
<wp:postmeta><wp:meta_key>_thumbnail_id</wp:meta_key><wp:meta_value>12</wp:meta_value></wp:postmeta>
|
|
<wp:postmeta><wp:meta_key>_product_image_gallery</wp:meta_key><wp:meta_value>12</wp:meta_value></wp:postmeta>
|
|
<wp:postmeta><wp:meta_key>_elementor_data</wp:meta_key><wp:meta_value><![CDATA['''+widgets+''']]></wp:meta_value></wp:postmeta>
|
|
<wp:postmeta><wp:meta_key>_price</wp:meta_key><wp:meta_value>1200</wp:meta_value></wp:postmeta>
|
|
</item></channel></rss>'''
|
|
response=factory.post(f'/api/sites/{sid}/import',files={'file':('export.xml',wxr.encode(),'application/xml')})
|
|
assert response.status_code==200,response.text
|
|
item=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['title']=='产品A')
|
|
assert item['cover']=='https://img.example/cover.jpg'
|
|
assert item['meta']['fields']['category']=='运动型多用途车'
|
|
assert item['meta']['fields']['price']=='1200'
|
|
assert '核心价值' in item['body']
|
|
assert item['meta']['migration_warnings']
|
|
assert item['meta']['fields']['gallery']==['https://img.example/cover.jpg']
|
|
|
|
|
|
def test_excel_mapping_relations_preview_and_repeat(factory):
|
|
sid=site(factory)
|
|
w=Workbook();ws=w.active;ws.append(['车型名称','品牌','分类','动力形式','图库','原网址'])
|
|
ws.append(['SUV Alpha','Hesu','SUV','PHEV','https://img.example/1.jpg;https://img.example/2.jpg','/cars/alpha.html'])
|
|
b=io.BytesIO();w.save(b)
|
|
r=factory.post(f'/api/factory/sites/{sid}/mapping/parse-file',files={'file':('cars.xlsx',b.getvalue())})
|
|
assert r.status_code==200,r.text
|
|
parsed=r.json();assert parsed['count']==1
|
|
assert parsed['suggested']['title']=='车型名称'
|
|
report=factory.post(f'/api/factory/sites/{sid}/mapping/run',json={'rows':parsed['rows'],
|
|
'rules':{**parsed['suggested'],'gallery':'图库'},'content_type':'vehicle','dry_run':True}).json()
|
|
assert report['mapped']==1 and report['sample'][0]['route']=='/cars/alpha.html'
|
|
committed=factory.post(f'/api/factory/sites/{sid}/mapping/run',json={'rows':parsed['rows'],
|
|
'rules':{**parsed['suggested'],'gallery':'图库'},'content_type':'vehicle','dry_run':False})
|
|
assert committed.status_code==200
|
|
x=factory.get(f'/api/factory/sites/{sid}/relationships').json()
|
|
assert any(i['brand']=='Hesu' for i in x['brand_pages_missing'])
|
|
y=factory.post(f'/api/factory/sites/{sid}/relationships/assemble').json()
|
|
assert y['count']==1
|
|
assert factory.post(f'/api/factory/sites/{sid}/relationships/assemble').json()['count']==0
|
|
entries=factory.get(f'/api/sites/{sid}/entries').json()
|
|
assert any(e['type']=='brand' and e['title']=='Hesu' for e in entries)
|
|
assert next(e for e in entries if e['title']=='SUV Alpha')['meta']['fields']['gallery']==['https://img.example/1.jpg','https://img.example/2.jpg']
|
|
for theme in ('industrial','corporate','minimal'):
|
|
assert factory.put(f'/api/factory/sites/{sid}/theme',json={'theme':theme}).status_code==200
|
|
version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
assert 'SUV Alpha' in factory.get(f'/preview/{sid}/{version}/cars/alpha.html').text
|
|
|
|
|
|
def test_migration_manifest_and_built_qa_blocks_missing(factory):
|
|
sid=site(factory,mode='migration')
|
|
baseline='url,seo_title,description,canonical,status\nhttps://old.example/,Title from old,Old description,,200\nhttps://old.example/legacy-only.html,Missing,,,200\n'
|
|
r=factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('urls.csv',baseline.encode())})
|
|
assert r.status_code==200,r.text
|
|
assert r.json()['imported']==2
|
|
version=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
report=factory.get(f'/api/audit/sites/{sid}/builds/{version}/report').json()
|
|
assert not report['passed'] and report['errors']>=1
|
|
assert any(i['route']=='/legacy-only.html' and i['field']=='url' for i in report['issues'])
|
|
assert any(i['route']=='/' and i['severity']=='warning' for i in report['issues'])
|
|
readiness=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={version}').json()
|
|
assert not readiness['can_publish']
|
|
csv=factory.get(f'/api/audit/sites/{sid}/builds/{version}/report.csv')
|
|
assert csv.status_code==200 and 'legacy-only.html' in csv.text
|
|
|
|
|
|
def test_sitemap_import_and_invalid_baseline_atomic(factory):
|
|
sid=site(factory,mode='migration')
|
|
xml='''<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://old.example/about/</loc></url></urlset>'''
|
|
assert factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('sitemap.xml',xml.encode())}).json()['imported']==1
|
|
bad='url\nhttps://old.example/about/\nhttps://old.example/about/?page=5\n'
|
|
response=factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('bad.csv',bad.encode())})
|
|
assert response.status_code==200 and response.json()['errors']
|
|
baseline=factory.get(f'/api/enterprise/sites/{sid}/baseline/report').json()
|
|
assert baseline['baseline_count']==1
|
|
|
|
|
|
def test_theme_gallery_and_compatibility(factory):
|
|
sid=site(factory)
|
|
for theme in ('automotive','industrial','corporate','minimal'):
|
|
r=factory.get(f'/api/factory/themes/{theme}/compatibility')
|
|
assert r.status_code==200 and r.json()['valid'],r.text
|
|
product=factory.post(f'/api/sites/{sid}/entries',json={'title':'Unique Product','type':'product','route':'/product/unique/',
|
|
'cover':'https://imgs.example/cover.jpg','meta':{'fields':{'gallery':['https://imgs.example/cover.jpg','https://imgs.example/second.jpg']}}})
|
|
assert product.status_code in (200,201),product.text
|
|
ver=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
html=factory.get(f'/preview/{sid}/{ver}/product/unique/').text
|
|
assert 'pro-gallery-thumbs' in html and 'data-gallery-image' in html
|
|
assert 'pro.js' in html
|
|
|
|
|
|
def test_css_style_breakout_rejected(factory):
|
|
z=io.BytesIO()
|
|
with zipfile.ZipFile(z,'w') as f:
|
|
data={'id':'test-css-escape','version':'1.0.0','name':'Safe Test','layout_family':'corporate'}
|
|
f.writestr('manifest.json',json.dumps(data));f.writestr('page-presets.json',json.dumps({'home':[]}))
|
|
f.writestr('theme.css','body{color:red}</style><script>alert(1)</script>')
|
|
result=factory.post('/api/factory/themes/install',files={'file':('unsafe.zip',z.getvalue())})
|
|
assert result.status_code==400
|
|
|
|
def test_migration_warning_requires_explicit_review(factory):
|
|
sid=site(factory,mode='migration')
|
|
source='url,seo_title\nhttps://old.example/,Old SEO Title\n'
|
|
factory.post(f'/api/audit/sites/{sid}/import-baseline',files={'file':('old.csv',source.encode())})
|
|
build=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
current=factory.get(f'/api/audit/sites/{sid}/builds/{build}/report').json()
|
|
warnings=[i for i in current['issues'] if i['severity']=='warning' and i['field']=='title']
|
|
assert warnings and current['unreviewed_warnings']>=1
|
|
readiness=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={build}').json()
|
|
assert not readiness['can_publish']
|
|
assert any('未经审核' in x['message'] for x in readiness['gates'])
|
|
denied=factory.post(f'/api/audit/sites/{sid}/builds/{build}/review-warning',json={
|
|
'issue_id':warnings[0]['issue_id'],'reason':'短'});
|
|
assert denied.status_code==400
|
|
accepted=factory.post(f'/api/audit/sites/{sid}/builds/{build}/review-warning',json={
|
|
'issue_id':warnings[0]['issue_id'],'reason':'企业管理人员已经核查该 SEO 标题调整,并批准本构建中的改动。'})
|
|
assert accepted.status_code==200,accepted.text
|
|
after=factory.get(f'/api/audit/sites/{sid}/builds/{build}/report').json()
|
|
assert after['unreviewed_warnings']==0
|
|
next_gate=factory.get(f'/api/factory/sites/{sid}/migration/readiness?build_id={build}').json()
|
|
assert not any('未经审核' in x['message'] for x in next_gate['gates'])
|
|
# Unrelated broken links may still correctly block publication.
|
|
|
|
|
|
def test_site_scoped_reusable_block_library(factory):
|
|
first=site(factory); second=site(factory)
|
|
block=blueprints.new_block('layout',{'columns':[{'width':1,'blocks':[blueprints.new_block('richtext',{'title':'跨页面共用模块'})]}]})
|
|
created=factory.post(f'/api/sites/{first}/components',json={'name':'品牌单列布局','block':block})
|
|
assert created.status_code==200,created.text
|
|
cid=created.json()['id']
|
|
rows=factory.get(f'/api/sites/{first}/components').json()
|
|
assert len(rows)==1 and rows[0]['block']['columns'][0]['blocks'][0]['title']=='跨页面共用模块'
|
|
assert not factory.get(f'/api/sites/{second}/components').json()
|
|
assert factory.put(f'/api/sites/{first}/components/{cid}',json={'name':'新版', 'expected_version':0,'block':block}).status_code==409
|
|
assert factory.put(f'/api/sites/{first}/components/{cid}',json={'name':'新版', 'expected_version':1,'block':block}).json()['version']==2
|
|
assert factory.delete(f'/api/sites/{first}/components/{cid}').json()['deleted']
|
|
|
|
|
|
def test_contact_form_requires_configured_https_endpoint(factory):
|
|
sid=site(factory)
|
|
assert factory.put(f'/api/sites/{sid}/contact-settings',json={'email':'hello@example.org', 'form_endpoint':'http://unsecure.example/forms'}).status_code==400
|
|
config=factory.put(f'/api/sites/{sid}/contact-settings',json={
|
|
'email':'hello@example.org','form_endpoint':'https://forms.example.org/receive','footer_text':'Official inquiry channel'})
|
|
assert config.status_code==200,config.text
|
|
factory.post(f'/api/factory/sites/{sid}/bootstrap')
|
|
build=factory.post(f'/api/sites/{sid}/builds',json={}).json()['version']
|
|
contact=factory.get(f'/preview/{sid}/{build}/contact/').text
|
|
assert '<form' in contact and 'forms.example.org/receive' in contact
|
|
assert 'Official inquiry channel' in contact
|