Files
cms-SiteForge/tests/test_multilingual_v815.py
浅念 601451f22b upgrade: adopt V8.21.3 (V8.13→8.21.3: P5 SFTP/S3, P6 multilingual, editor overhaul, enterprise hardening)
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>
2026-10-23 12:31:03 +08:00

108 lines
6.2 KiB
Python

"""P6 local deterministic tests: locale associations, SEO reciprocity, draft safety."""
from pathlib import Path
import re
import pytest
from fastapi.testclient import TestClient
from app import db, multilingual, builder
from app.main import app
@pytest.fixture
def demo(tmp_path, monkeypatch):
monkeypatch.setattr(db,'DATA',tmp_path)
monkeypatch.setattr(db,'DB_FILE',tmp_path/'siteforge.sqlite3')
monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','very-long-test-password-not-production')
monkeypatch.setenv('SITEFORGE_SESSION_SECRET','v815-ci-session-key-testing-only-not-a-secret')
with TestClient(app) as client:
assert client.post('/api/auth/login',json={'username':'admin','password':'very-long-test-password-not-production'}).status_code==200
csrf=client.get('/api/auth/me').json()['csrf']
client.headers.update({'X-SiteForge-CSRF':csrf})
def make(name,domain):
x=client.post('/api/sites',json={'name':name,'domain':domain,'theme':'corporate'})
assert x.status_code==201,x.text
return x.json()['id']
yield client,make,tmp_path
def post(client,path,body):
r=client.post(path,json=body)
assert r.status_code in (200,201),r.text
return r.json()
def prepare(client,make):
chinese=make('Chinese site','https://zh.example.org')
english=make('English site','https://en.example.org')
group=post(client,'/api/i18n/groups',{'name':'Hesu Global','default_locale':'en'})
gid=group['id']
post(client,f'/api/i18n/groups/{gid}/sites',{'site_id':chinese,'locale':'zh-CN'})
post(client,f'/api/i18n/groups/{gid}/sites',{'site_id':english,'locale':'en'})
a=post(client,f'/api/sites/{chinese}/entries',{'type':'page','title':'关于我们','route':'/about/','body':'<p>中文内容</p>'})['id']
b=post(client,f'/api/sites/{english}/entries',{'type':'page','title':'About','route':'/company/','body':'<p>English content</p>'})['id']
unit=post(client,f'/api/i18n/groups/{gid}/units',{'reference_key':'about-page'})['id']
post(client,f'/api/i18n/groups/{gid}/units/{unit}/members',{'site_id':chinese,'entry_id':a,'state':'approved'})
post(client,f'/api/i18n/groups/{gid}/units/{unit}/members',{'site_id':english,'entry_id':b,'state':'approved'})
return chinese,english,gid,unit
def test_reciprocal_hreflang_canonical_sitemap_and_language(demo):
client,make,tmp=demo
zh,en,gid,unit=prepare(client,make)
for sid,route,locale in [(zh,'about','zh-CN'),(en,'company','en')]:
result=builder.build(sid)
assert result['locale']==locale and result['alternate_pages']==1
build_id=result['version']
dist=tmp/'builds'/sid/build_id/'dist'
page=(dist/route/'index.html').read_text()
assert '<html lang="'+locale+'"' in page
assert '<link rel="alternate" hreflang="zh-CN" href="https://zh.example.org/about/">' in page
assert '<link rel="alternate" hreflang="en" href="https://en.example.org/company/">' in page
assert '<link rel="alternate" hreflang="x-default" href="https://en.example.org/company/">' in page
sitemap=(dist/'sitemap.xml').read_text()
assert 'xmlns:xhtml="http://www.w3.org/1999/xhtml"' in sitemap
assert 'xhtml:link rel="alternate" hreflang="en"' in sitemap
# No duplicate locale-specific path for homepage when there is no translation link.
assert 'hreflang="en"' not in (dist/'index.html').read_text()
detail=client.get('/api/i18n/sites/'+zh)
assert detail.status_code==200
assert len(detail.json()['units'])==2
def test_pending_translation_cannot_be_published_or_linked(demo):
client,make,tmp=demo
zh,en,gid,unit=prepare(client,make)
draft=post(client,f'/api/sites/{en}/entries',{'type':'page','title':'Pending English','route':'/pending/','status':'draft'})['id']
new_unit=post(client,f'/api/i18n/groups/{gid}/units',{'reference_key':'pending-page'})['id']
assert client.post(f'/api/i18n/groups/{gid}/units/{new_unit}/members',json={'site_id':en,'entry_id':draft,'state':'pending'}).status_code==200
assert multilingual.alternates(en).get('/pending/') is None
db.execute('UPDATE entries SET status=? WHERE id=?',('published',draft))
with pytest.raises(ValueError,match='待翻译'):
builder.build(en)
def test_group_validation_and_security_conflicts(demo):
client,make,tmp=demo
zh,en,gid,unit=prepare(client,make)
forbidden=make('Duplicate domain','https://zh.example.org')
assert client.post(f'/api/i18n/groups/{gid}/sites',json={'site_id':forbidden,'locale':'fr-FR'}).status_code==409
third=make('French','https://fr.example.org')
assert client.post(f'/api/i18n/groups/{gid}/sites',json={'site_id':third,'locale':'en'}).status_code==409
assert client.post(f'/api/i18n/groups/{gid}/sites',json={'site_id':third,'locale':'??'}).status_code==400
foreign=post(client,f'/api/sites/{third}/entries',{'title':'Page in other site','route':'/t/'})['id']
assert client.post(f'/api/i18n/groups/{gid}/units/{unit}/members',json={'site_id':zh,'entry_id':foreign,'state':'approved'}).status_code==404
home=db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(zh,'/'))['id']
assert client.post(f'/api/i18n/groups/{gid}/units/{unit}/members',json={'site_id':zh,'entry_id':home,'state':'pending'}).status_code==409
unapproved=db.one('SELECT id FROM entries WHERE site_id=? AND route=?',(zh,'/about/'))['id']
assert client.post(f'/api/i18n/groups/{gid}/units/{unit}/members',json={'site_id':zh,'entry_id':unapproved,'state':'approved'}).status_code==200
def test_noindex_or_unapproved_peer_not_in_hreflang(demo):
client,make,tmp=demo
zh,en,gid,unit=prepare(client,make)
db.execute('UPDATE entries SET robots=? WHERE site_id=? AND route=?',('noindex,follow',en,'/company/'))
assert multilingual.alternates(zh)=={}
report=builder.build(zh)
html=(tmp/'builds'/zh/report['version']/'dist'/'about'/'index.html').read_text()
assert 'hreflang=' not in html
def test_injection_escape_and_valid_locale():
with pytest.raises(ValueError):multilingual.locale_value('<img onerror=x>')
assert multilingual.inject_seo('<!doctype html><html lang="zh-CN"><head></head><body>x</body>','fr-FR',[]) .find('lang="fr-FR"')>0