"""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':'

中文内容

'})['id'] b=post(client,f'/api/sites/{english}/entries',{'type':'page','title':'About','route':'/company/','body':'

English content

'})['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 '' in page assert '' in page assert '' 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('') assert multilingual.inject_seo('x','fr-FR',[]) .find('lang="fr-FR"')>0