129 lines
6.9 KiB
Python
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 '<script>' 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
|