"""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':'

我们的企业介绍

'})]}, {'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=''' 图片attachment12https://img.example/cover.jpg 产品Ahttps://wp.example/product/test-a/productpublish 运动型多用途车 _thumbnail_id12 _product_image_gallery12 _elementor_data _price1200 ''' 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='''https://old.example/about/''' 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 'auto-compiled.js' in html or '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}') 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 '