"""P4 whole-site chrome is editable, safe, versioned, and build-isolated.""" import json import pytest from fastapi import HTTPException from tests.test_factory import client,create from app import db,builder,site_design_v87 def _payload(**header): base=json.loads(json.dumps(site_design_v87.DEFAULTS)) base['header'].update({'show_announcement':True,'announcement':'V87_GLOBAL_DRAFT_MARKER', 'cta_text':'Get Quote V87','cta_href':'/contact/'}) base['header'].update(header) base['footer'].update({'title':'V87_ENTERPRISE_FOOTER','description':'International operations', 'email':'team@example.test','show_nav':True}) base['nav']=[{'title':'首页','route':'/'},{'title':'Solutions','route':'/services/', 'children':[{'title':'Consulting','route':'/about/'}]}] base['responsive'].update({'tablet_gap':72,'mobile_gap':33, 'mobile_container_padding':16,'mobile_hide_announcement':False}) return base def _get_page(sid,bid): return (db.DATA/'builds'/sid/bid/'dist'/'index.html').read_text('utf-8') def test_site_chrome_draft_isolation_publish_and_build(client): sid=create(client) before=builder.build(sid,False) marker='V87_GLOBAL_DRAFT_MARKER' assert marker not in _get_page(sid,before['version']) url=f'/api/sites/{sid}/site-design' payload=_payload() saved=client.put(url,json={'config':payload,'expected_revision':0}) assert saved.status_code==200,saved.text assert saved.json()['build_unchanged'] is True assert client.get(url).json()['state']=='draft' waiting=builder.build(sid,False) assert marker not in _get_page(sid,waiting['version']) published=client.post(url+'/publish',json={'expected_revision':1}) assert published.status_code==200,published.text after=builder.build(sid,False) html=_get_page(sid,after['version']) assert marker in html and 'V87_ENTERPRISE_FOOTER' in html assert 'Get Quote V87' in html and 'Consulting' in html css=(db.DATA/'builds'/sid/after['version']/'dist/assets/custom.css').read_text('utf-8') assert 'padding-block:33px' in css and 'padding-left:16px' in css assert len(client.get(url+'/publications').json())==1 assert client.get(url).json()['state']=='published' assert marker not in _get_page(sid,before['version']) @pytest.mark.parametrize('theme',['automotive','industrial','corporate','minimal']) def test_whole_site_theme_render_uses_published_design(client,theme): sid=create(client,theme) url=f'/api/sites/{sid}/site-design' payload=_payload() assert client.put(url,json={'config':payload,'expected_revision':0}).status_code==200 assert client.post(url+'/publish',json={'expected_revision':1}).status_code==200 result=builder.build(sid,False) html=_get_page(sid,result['version']) assert 'V87_GLOBAL_DRAFT_MARKER' in html assert 'Get Quote V87' in html assert 'team@example.test' in html assert 'Consulting' in html def test_site_design_preview_is_not_committed(client): sid=create(client);url=f'/api/sites/{sid}/site-design' assert client.put(url,json={'config':_payload(),'expected_revision':0}).status_code==200 eid=next(x['id'] for x in client.get(f'/api/sites/{sid}/entries').json() if x['route']=='/') resp=client.post(f'/api/sites/{sid}/pages/{eid}/editor-preview',json={ 'blocks':[],'page_template':'home','layout_mode':'append','site_design':_payload()}) assert resp.status_code==200,resp.text[:600] assert 'V87_GLOBAL_DRAFT_MARKER' in resp.text assert 'V87_GLOBAL_DRAFT_MARKER' not in _get_page(sid,builder.build(sid,False)['version']) assert '全站页眉与页脚' in client.get(f'/editor/{sid}/{eid}').text assert 'showSiteDesign' in client.get('/ui/editor.js').text def test_conflicting_versions_and_website_updates(client): sid=create(client);url=f'/api/sites/{sid}/site-design' assert client.put(url,json={'config':_payload(),'expected_revision':0}).status_code==200 assert client.put(url,json={'config':_payload(),'expected_revision':0}).status_code==409 assert client.post(url+'/publish',json={'expected_revision':0}).status_code==409 assert client.put(f'/api/enterprise/sites/{sid}/design-tokens',json={'heading_scale':1.3}).status_code==200 assert client.get(url).json()['conflict'] is True assert client.post(url+'/publish',json={'expected_revision':1}).status_code==409 assert client.get(url+'/publications').json()==[] def test_invalid_navigation_and_restricted_responsive_values(client): sid=create(client);url=f'/api/sites/{sid}/site-design' for bad in [ {'nav':[{'title':'Bad','route':'javascript:alert(1)'}]}, {'nav':[{'title':'Bad','route':'//169.254.169.254/'}]}, {'nav':[{'title':'Bad','route':'/','children':[{'title':'A','route':'/','children':[{'title':'B','route':'/', 'children':[{'title':'C','route':'/'}]}]}]}]}, {'responsive':{'mobile_gap':-1}}, {'responsive':{'mobile_gap':'calc(1px * 3)'}}, {'header':{'announcement':'x'*141}}, {'footer':{'title':'','unknown':'a'}}, ]: payload=_payload() for k,v in bad.items(): if isinstance(v,dict):payload[k].update(v) else:payload[k]=v response=client.put(url,json={'config':payload,'expected_revision':0}) assert response.status_code==400,(bad,response.text) assert client.get(url).json()['revision']==0 def test_cancel_draft_and_multi_tenant_isolation(client): sid=create(client);other=create(client) url=f'/api/sites/{sid}/site-design' assert client.put(url,json={'config':_payload(),'expected_revision':0}).status_code==200 assert client.get(f'/api/sites/{other}/site-design').json()['state']=='published' assert client.delete(url).status_code==200 assert client.get(url).json()['revision']==0 assert client.post(url+'/publish',json={'expected_revision':0}).status_code==409 assert 'V87_GLOBAL_DRAFT_MARKER' not in _get_page(other,builder.build(other,False)['version']) def test_empty_nav_preserves_legacy_navigation_and_default_theme(client): sid=create(client) r=client.get(f'/api/sites/{sid}/site-design').json() assert r['state']=='published' and r['config']['nav']==[] html=_get_page(sid,builder.build(sid,False)['version']) assert '首页' in html and '产品' in html def test_legacy_direct_design_writes_blocked_by_default(client,monkeypatch): sid=create(client) eid=next(x['id'] for x in client.get(f'/api/sites/{sid}/entries').json() if x['route']=='/') monkeypatch.delenv('SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE',raising=False) assert client.put(f'/api/sites/{sid}/pages/{eid}/blueprint',json={ 'blocks':[],'page_template':'home'}).status_code==409 assert client.post(f'/api/sites/{sid}/pages/{eid}/blueprint/preset',json={ 'page_template':'home'}).status_code==409 assert client.put(f'/api/sites/{sid}/navigation',json=[{'title':'Bad','route':'/'}]).status_code==409 assert client.put(f'/api/enterprise/sites/{sid}/design-tokens',json={ 'font':'serif'}).status_code==409 assert client.patch(f'/api/sites/{sid}/entries/{eid}',json={ 'meta':{'blocks':[]}}).status_code==409 # New draft APIs work while legacy direct-write interfaces are disabled. assert client.put(f'/api/sites/{sid}/site-design',json={ 'config':_payload(),'expected_revision':0}).status_code==200 assert client.get(f'/api/sites/{sid}/site-design').json()['state']=='draft' def test_site_design_tokens_follow_draft_publish_boundary(client): sid=create(client) original=builder.build(sid,False) path=f'/api/sites/{sid}/site-design' config=_payload();config['tokens'].update({'font':'serif','container_width':1380,'heading_scale':1.2}) assert client.put(path,json={'config':config,'expected_revision':0}).status_code==200 waiting=builder.build(sid,False) css=lambda bid:(db.DATA/'builds'/sid/bid/'dist/assets/custom.css').read_text('utf-8') assert '--sf-width:1380px' not in css(original['version']) assert '--sf-width:1380px' not in css(waiting['version']) assert client.post(path+'/publish',json={'expected_revision':1}).status_code==200 done=builder.build(sid,False) assert '--sf-width:1380px' in css(done['version']) and 'Georgia' in css(done['version']) assert client.get(f'/api/enterprise/sites/{sid}/design-tokens').json()['container_width']==1380 def test_legacy_frontends_use_drafts(client): script=client.get('/ui/admin.js').text enterprise_script=client.get('/ui/enterprise-ui.js').text assert "'/site-design'" in script or "'/site-design'" in script.replace('"',"'") assert '/site-design' in enterprise_script assert 'design-tokens`,' not in enterprise_script