Files
浅念 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

129 lines
6.9 KiB
Python

"""Deterministic P4 regression checks. Drafts must never leak into builds."""
import json
import pytest
from tests.test_factory import client,create
from app import blueprints,db,v8_templates
def _home(c,sid):
return next(x for x in c.get(f'/api/sites/{sid}/entries').json() if x['route']=='/')
def test_six_new_schema_blocks_and_safe_rendering():
specs=blueprints.BLOCK_PROPERTY_SCHEMA
kinds=('carousel','video','tabs','pricing','map','form_link')
assert all(k in specs for k in kinds)
blocks=[blueprints.new_block(x) for x in kinds]
assert len(blueprints.validate_blocks(blocks))==6
video=blueprints.new_block('video',{'url':'https://www.youtube.com/watch?v=dQw4w9WgXcQ'})
html=blueprints.render_blocks([video],{}, {}, [])
assert 'youtube-nocookie.com/embed/dQw4w9WgXcQ' in html
assert 'iframe' in html
blocked=blueprints.new_block('video',{'url':'https://www.evil.example/video/123'})
assert 'iframe' not in blueprints.render_blocks([blocked],{}, {}, [])
item=blueprints.new_block('pricing',{'items':[{'name':'<script>alert(1)</script>', 'price':'$5','features':['<img src=x onerror=alert(1)>'], 'href':'javascript:alert(1)'}]})
text=blueprints.render_blocks([item],{}, {}, [])
assert '<script>' not in text and '<img src=x' not in text and 'javascript:' not in text
assert '&lt;script&gt;' in text
def test_malformed_and_oversized_blocks_rejected():
with pytest.raises(ValueError):blueprints.validate_blocks([blueprints.new_block('tabs',{'items':[{'text':'a'}]*17})])
with pytest.raises(ValueError):blueprints.validate_blocks([blueprints.new_block('pricing',{'items':[{'features':['a']*13}]})])
def test_editor_draft_is_separate_until_published(client):
sid=create(client)
eid=_home(client,sid)['id']
before=db.one('SELECT meta_json FROM entries WHERE id=? AND site_id=?',(eid,sid))['meta_json']
draft=blueprints.new_block('richtext',{'title':'Design unpublished V86'})
req={'blocks':[draft],'page_template':'home','layout_mode':'replace','expected_revision':0}
saved=client.put(f'/api/sites/{sid}/pages/{eid}/studio-draft',json=req)
assert saved.status_code==200,saved.text
assert saved.json()['revision']==1
assert db.one('SELECT meta_json FROM entries WHERE id=? AND site_id=?',(eid,sid))['meta_json']==before
read=client.get(f'/api/sites/{sid}/pages/{eid}/studio-draft').json()
assert read['blocks'][0]['title']=='Design unpublished V86'
assert read['entry']['route']=='/'
pub=client.post(f'/api/sites/{sid}/pages/{eid}/studio-draft/publish',json={'expected_revision':1})
assert pub.status_code==200,pub.text
after=db.one('SELECT meta_json FROM entries WHERE site_id=? AND id=?',(sid,eid))['meta_json']
assert 'Design unpublished V86' in after and before!=after
assert client.get(f'/api/sites/{sid}/pages/{eid}/studio-draft').json()['state']=='published'
assert db.one('SELECT COUNT(*) n FROM studio_publications WHERE site_id=?',(sid,))['n']==1
def test_draft_revision_and_changed_entry_conflicts(client):
sid=create(client);eid=_home(client,sid)['id']
endpoint=f'/api/sites/{sid}/pages/{eid}/studio-draft'
body={'blocks':[blueprints.new_block('cta',{'title':'draft'})], 'page_template':'home','expected_revision':0}
assert client.put(endpoint,json=body).status_code==200
assert client.put(endpoint,json=body).status_code==409
assert client.post(endpoint+'/publish',json={'expected_revision':0}).status_code==409
row=db.one('SELECT * FROM entries WHERE site_id=? AND id=?',(sid,eid))
db.execute('UPDATE entries SET title=? WHERE id=?',('Changed elsewhere',eid))
assert client.post(endpoint+'/publish',json={'expected_revision':1}).status_code==409
assert client.get(endpoint).json()['conflict'] is True
assert db.one('SELECT title FROM entries WHERE id=?',(eid,))['title']=='Changed elsewhere'
def test_discard_and_site_isolation(client):
sid=create(client);other=create(client);eid=_home(client,sid)['id']
endpoint=f'/api/sites/{sid}/pages/{eid}/studio-draft'
assert client.put(endpoint,json={'blocks':[],'page_template':'home','expected_revision':0}).status_code==200
assert client.get(f'/api/sites/{other}/pages/{eid}/studio-draft').status_code==404
assert client.delete(endpoint).status_code==200
assert client.get(endpoint).json()['revision']==0
assert client.post(endpoint+'/publish',json={'expected_revision':0}).status_code==409
def test_draft_preset_and_property_contract(client):
sid=create(client);eid=_home(client,sid)['id']
r=client.get(f'/api/sites/{sid}/pages/{eid}/studio-draft/preset?page_template=home')
assert r.status_code==200 and r.json()['blocks']
assert client.get('/api/editor/blocks').json()['property_schemas']['carousel']['allows_arbitrary_code'] is False
def test_whole_site_preflight_is_read_only(client):
sid=create(client)
snap=v8_templates.export_template(sid,'Sample')
assert snap['theme_version']
p=client.post(f'/api/factory/sites/{sid}/whole-site-templates/preflight',json={'template':snap})
assert p.status_code==200,p.text
assert p.json()['compatible'] is True and p.json()['no_changes_applied']
assert p.json()['existing_routes']==['/']
broken={**snap,'theme_version':'99.0.0'}
fail=client.post(f'/api/factory/sites/{sid}/whole-site-templates/preflight',json={'template':broken})
assert fail.status_code==200 and fail.json()['compatible'] is False
assert client.post(f'/api/factory/sites/{sid}/whole-site-templates/apply',json={'template':broken,'confirm':True,'overwrite_layouts':True}).status_code==422
def test_draft_not_rendered_in_static_build_until_publish(client):
from app import builder
from pathlib import Path
import zipfile
sid=create(client)
eid=_home(client,sid)['id']
title='V86_PRIVATE_UNPUBLISHED_DESIGN'
payload={'blocks':[blueprints.new_block('richtext',{'title':title,'body':'<p>release barrier</p>'})],
'page_template':'home','layout_mode':'replace','expected_revision':0}
endpoint=f'/api/sites/{sid}/pages/{eid}/studio-draft'
assert client.put(endpoint,json=payload).status_code==200
before=builder.build(sid,False)
with zipfile.ZipFile(db.DATA/'archives'/f'{sid}-{before["version"]}.zip') as archive:
assert title not in archive.read('index.html').decode('utf-8')
assert client.post(endpoint+'/publish',json={'expected_revision':1}).status_code==200
after=builder.build(sid,False)
with zipfile.ZipFile(db.DATA/'archives'/f'{sid}-{after["version"]}.zip') as archive:
assert title in archive.read('index.html').decode('utf-8')
def test_editor_page_uses_draft_ui(client):
sid=create(client)
eid=_home(client,sid)['id']
html=client.get(f'/editor/{sid}/{eid}')
assert html.status_code==200
assert '发布设计' in html.text and '保存草稿' in html.text
js=client.get('/ui/editor.js').text
assert '/studio-draft' in js and 'publishDesign' in js