108 lines
6.2 KiB
Python
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
|