601451f22b
Overlay ~150 upstream items onto our V8.12 base, preserving trade-pro
theme, registry tests, and design docs. Re-applied our patch set
(discovery/env_for/blocks.css/type-mapping/theme_color/frontend entries,
test_theme_pro_v3 mods) — all anchors survived upstream evolution.
New platform enhancement (fixes upgrade-introduced regression):
multilingual.site_locale now honors settings_json.locale, plus
PUT /api/i18n/sites/{id}/locale — standalone English sites get
<html lang='en'> under the V8.15 locale-rewrite pipeline.
compose: adopt V8.21.3 (image 8.21.3-dev.1, /api/ready probes, MySQL/S3
env) with project name pinned to siteforge-v8-10 (data volumes).
Tests: 356 passed / 11 failed — all 11 confirmed pre-existing in
pristine V8.21.3 on this host (10 upstream Windows env + our build test
now green after locale fix). Patch registry updated in plan doc.
Co-Authored-By: Claude Code <noreply@anthropic.com>
125 lines
6.1 KiB
Python
125 lines
6.1 KiB
Python
"""V8.20 offline CMS compatibility tests, independent of the public network."""
|
|
import io
|
|
import json
|
|
import pytest
|
|
from app import wordpress_compat
|
|
from app.wp_complex_v820 import inspect_wxr, extract_acf
|
|
from app.wp_migration_v82 import _media_urls, _rewrite_media_html
|
|
from test_factory_v2 import factory,site
|
|
|
|
|
|
def wxr(records):
|
|
return ('''<?xml version="1.0"?><rss version="2.0" xmlns:wp="http://wordpress.org/export/1.2/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel>'''+records+'</channel></rss>').encode()
|
|
|
|
|
|
def entry(wp_id,link,custom='',body='',kind='page'):
|
|
return f'''<item><title>Page {wp_id}</title><link>{link.replace('&','&')}</link>
|
|
<wp:post_type>{kind}</wp:post_type><wp:post_id>{wp_id}</wp:post_id>
|
|
<wp:status>publish</wp:status><content:encoded><![CDATA[{body}]]></content:encoded>{custom}</item>'''
|
|
|
|
|
|
def meta(key,value):
|
|
return f'<wp:postmeta><wp:meta_key>{key}</wp:meta_key><wp:meta_value><![CDATA[{value}]]></wp:meta_value></wp:postmeta>'
|
|
|
|
|
|
def test_wxr_duplicate_route_and_dynamic_query_blocked():
|
|
data=wxr(entry(1,'https://old.example/products/a/')+entry(2,'https://old.example/products/a/')+entry(3,'https://old.example/?p=5'))
|
|
out=inspect_wxr(data)
|
|
assert out['unique_published_routes']==1
|
|
assert out['blocking_issues']==2
|
|
assert {x['code'] for x in out['issues']}=={'duplicate_route','invalid_permalink'}
|
|
assert out['ready_for_automatic_publish'] is False
|
|
|
|
|
|
def test_wxr_counts_attachments_and_custom_types():
|
|
data=wxr(entry(1,'https://old.example/a/',kind='attachment')+entry(2,'https://old.example/b/',kind='portfolio'))
|
|
out=inspect_wxr(data)
|
|
assert out['counts']['attachments']==1
|
|
assert out['features']['custom_post_types']==1
|
|
assert out['sample_routes'][0]['route']=='/b/'
|
|
|
|
|
|
def test_wxr_dynamic_shortcode_not_claimed_as_static():
|
|
data=wxr(entry(1,'https://old.example/contact/',body='[contact-form-7 id="4"]'))
|
|
out=inspect_wxr(data)
|
|
assert out['blocking_issues']==1
|
|
assert out['issues'][0]['code']=='dynamic_shortcode'
|
|
|
|
|
|
def test_acf_simple_repeater_explicit_field_references():
|
|
values={'_features':'field_parent','features':'2','features_0_title':'First','_features_0_title':'field_child',
|
|
'features_1_title':'Second','_features_1_title':'field_child','headline':'About','_headline':'field_header'}
|
|
parsed=extract_acf(values)
|
|
assert parsed['repeaters']['features']==[{'title':'First'},{'title':'Second'}]
|
|
assert parsed['fields']['headline']=='About'
|
|
assert any('核对' in s for s in parsed['warnings'])
|
|
|
|
|
|
def test_acf_serialized_does_not_execute_or_decode():
|
|
parsed=extract_acf({'_links':'field_test','links':'a:1:{i:0;s:4:"evil";}'})
|
|
assert 'links' not in parsed['fields']
|
|
assert any('序列化' in s for s in parsed['warnings'])
|
|
|
|
|
|
def test_acf_incomplete_repeater_kept_raw_only():
|
|
meta_values={'_list':'field_list','list':'2','_list_0_title':'field_title','list_0_title':'First'}
|
|
parsed=extract_acf(meta_values)
|
|
assert 'list' not in parsed['repeaters']
|
|
assert any('不连续' in x for x in parsed['warnings'])
|
|
|
|
|
|
def test_elementor_unknown_widget_reported():
|
|
data=json.dumps([{'elType':'widget','widgetType':'unknown_gallery','settings':{}},
|
|
{'elType':'widget','widgetType':'heading','settings':{'title':'A & B'}}])
|
|
out=wordpress_compat.elementor_to_html(data)
|
|
assert out['widgets']==2
|
|
assert 'A & B' in out['html']
|
|
assert any('unknown_gallery' in x for x in out['warnings'])
|
|
|
|
|
|
def test_css_remote_image_inventory_and_rewrite():
|
|
source={'body':'''<div style="background-image: url('https://old.example/bg.webp')"><img data-background="https://old.example/lazy.png"></div>'''}
|
|
assert _media_urls(source)==['https://old.example/bg.webp','https://old.example/lazy.png']
|
|
converted={'https://old.example/bg.webp':'/media/a.webp','https://old.example/lazy.png':'/media/b.png'}
|
|
result,changed=_rewrite_media_html(source['body'],converted)
|
|
assert changed and '/media/a.webp' in result and '/media/b.png' in result
|
|
assert not _media_urls({'body':result})
|
|
|
|
|
|
def test_xxe_and_invalid_input_are_rejected():
|
|
with pytest.raises(ValueError):inspect_wxr(b'')
|
|
with pytest.raises(Exception):inspect_wxr(b'<!DOCTYPE rss [<!ENTITY x SYSTEM "file:///etc/passwd">]><rss>&x;</rss>')
|
|
with pytest.raises(ValueError):inspect_wxr(b'<urlset/>')
|
|
|
|
|
|
def test_wxr_api_enforces_auth_and_csrf(factory):
|
|
sid=site(factory,mode='migration')
|
|
payload=wxr(entry(1,'https://old.example/about/'))
|
|
response=factory.post(f'/api/migrations/sites/{sid}/wordpress/wxr-preflight',files={'file':('site.xml',payload)})
|
|
assert response.status_code==200,response.text
|
|
assert response.json()['unique_published_routes']==1
|
|
assert response.json()['ready_for_automatic_publish'] is False
|
|
bad=factory.post(f'/api/migrations/sites/{sid}/wordpress/wxr-preflight',files={'file':('site.xml',b'<urlset/>')})
|
|
assert bad.status_code==400
|
|
|
|
|
|
def test_import_wxr_rejects_dynamic_path_before_writing(factory):
|
|
sid=site(factory,mode='migration')
|
|
payload=wxr(entry(1,'https://old.example/?p=13'))
|
|
response=factory.post(f'/api/sites/{sid}/import',files={'file':('site.xml',payload)})
|
|
assert response.status_code in (400,422)
|
|
assert not any(x['route']=='/' and x['title']=='Page 1' for x in factory.get(f'/api/sites/{sid}/entries').json())
|
|
|
|
|
|
def test_wxr_acf_repeater_survives_import(factory):
|
|
sid=site(factory,mode='migration')
|
|
custom=''.join(meta(k,v) for k,v in {'_features':'field_parent','features':'2',
|
|
'_features_0_name':'field_child','features_0_name':'SUV',
|
|
'_features_1_name':'field_child','features_1_name':'Pickup'}.items())
|
|
payload=wxr(entry(1,'https://old.example/products/a/',custom=custom))
|
|
response=factory.post(f'/api/sites/{sid}/import',files={'file':('site.xml',payload)})
|
|
assert response.status_code==200,response.text
|
|
item=next(x for x in factory.get(f'/api/sites/{sid}/entries').json() if x['route']=='/products/a/')
|
|
assert item['meta']['fields']['features']==[{'name':'SUV'},{'name':'Pickup'}]
|
|
assert item['meta']['acf_repeaters']=={'features':[{'name':'SUV'},{'name':'Pickup'}]}
|