import io import json import zipfile from pathlib import Path import pytest from fastapi.testclient import TestClient from app import db from app.main import app from app.core import route_path,ensure_public_url @pytest.fixture def client(tmp_path,monkeypatch): monkeypatch.setattr(db,'DATA',tmp_path) monkeypatch.setattr(db,'DB_FILE',tmp_path/'test.sqlite3') monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','strong-supervisor-password-2026') monkeypatch.setenv('SITEFORGE_SESSION_SECRET','large-random-key-for-test-only-2026-the-test-environment') with TestClient(app) as c: assert c.get('/api/sites').status_code == 401 assert c.post('/api/auth/login',json={'password':'strong-supervisor-password-2026'}).status_code == 200 auth=c.get('/api/auth/me').json() c.headers.update({'X-SiteForge-CSRF': auth['csrf']}) yield c def create(c,theme='automotive'): r=c.post('/api/sites',json={'name':'Example Test','theme':theme,'domain':'https://example.test','mode':'migration'}) assert r.status_code==201,r.text return r.json()['id'] def test_site_and_theme_switch_preserve_content(client): a=create(client) b=create(client,'industrial') payload={'type':'product','title':'Product A','route':'/products/legacy.html','seo_title':'Original SEO Title','seo_description':'Original SEO Description','body':'

Hello world

'} x=client.post(f'/api/sites/{a}/entries',json=payload) assert x.status_code==201,x.text assert len(client.get(f'/api/sites/{b}/entries').json())==1 for theme in ('automotive','industrial','corporate','minimal'): assert client.patch(f'/api/sites/{a}',json={'theme':theme}).status_code==200 info=client.post(f'/api/sites/{a}/builds',json={'mirror_images':False}) assert info.status_code==200,info.text bid=info.json()['version'] resp=client.get(f'/preview/{a}/{bid}/products/legacy.html') assert resp.status_code==200,resp.text[:150] assert 'Original SEO Title' in resp.text assert '' in resp.text assert 'Original SEO Description' in resp.text assert 'Hello' in resp.text dl=client.get(f'/api/sites/{a}/builds/{bid}/download') with zipfile.ZipFile(io.BytesIO(dl.content)) as z: assert 'products/legacy.html' in z.namelist() assert 'assets/theme.css' in z.namelist() assert 'sitemap.xml' in z.namelist() assert len(client.get(f'/api/sites/{a}/entries').json())==2 def test_home_blueprint_and_static_links(client): sid=create(client) pages=client.get(f'/api/sites/{sid}/entries').json() home=pages[0] assert home['route']=='/' meta={'blocks':[{'type':'features','title':'优势清单','items':[{'title':'服务能力','text':'广泛覆盖'}]}, {'type':'cta','title':'马上合作','href':'/contact/'}]} assert client.patch(f'/api/sites/{sid}/entries/{home["id"]}',json={'meta':meta}).status_code==200 r=client.post(f'/api/sites/{sid}/builds',json={}) bid=r.json()['version'] txt=client.get(f'/preview/{sid}/{bid}/').text assert '优势清单' in txt and '马上合作' in txt def test_duplicate_routes_and_unsafe_html(client): sid=create(client) x=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'X','route':'/test/','body':'x'}) assert x.status_code==201 y=client.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Y','route':'/test/'}) assert y.status_code==400 z=client.get(f'/api/sites/{sid}/entries').json() it=next(x for x in z if x['route']=='/test/') assert '','visible':False}] res=client.post(f'/api/sites/{sid}/pages/{homepage["id"]}/editor-preview',json={'blocks':blocks,'page_template':'home'}) assert res.status_code==200,res.text assert 'onerror=alert' in res.text # text escaped, never a literal img tag assert '<img src=x onerror' in res.text assert 'href="/"' in res.text or 'href="./"' in res.text assert 'should be hidden' not in res.text assert client.put(f'/api/sites/{sid}/pages/{homepage["id"]}/blueprint',json={'blocks':[{'id':'bad-id','type':'script','title':'Bad'}],'page_template':'home'}).status_code==400 def test_bulk_theme_assembly_preserves_legacy_url_and_seo(client): sid=create(client,'industrial') resp=client.post(f'/api/sites/{sid}/entries',json={'type':'product','title':'HDPE Pipe','route':'/old-product/hdpe-pipe.html','body':'

Legacy Technical Details

','seo_title':'HDPE pipe SEO forever','seo_description':'Exact old metadata'}) assert resp.status_code==201 entry=resp.json() assert not entry['meta'].get('blocks') done=client.post(f'/api/sites/{sid}/apply-theme',json={'mode':'missing'}) assert done.status_code==200,done.text assert done.json()['updated']>=1 updated=client.get(f'/api/sites/{sid}/entries').json() final=next(x for x in updated if x['id']==entry['id']) assert final['route']=='/old-product/hdpe-pipe.html' assert final['seo_title']=='HDPE pipe SEO forever' assert len(final['meta']['blocks'])>=2 built=client.post(f'/api/sites/{sid}/builds',json={}) assert built.status_code==200,built.text html=client.get(f'/preview/{sid}/{built.json()["version"]}/old-product/hdpe-pipe.html').text assert 'Legacy Technical Details' in html assert 'HDPE pipe SEO forever' in html def test_scaffold_standard_pages_safe_idempotent(client): sid=create(client) first=client.post(f'/api/sites/{sid}/scaffold') assert first.status_code==200 assert '/about/' in first.json()['created'] second=client.post(f'/api/sites/{sid}/scaffold') assert second.status_code==200 and not second.json()['created'] lst=client.get(f'/api/sites/{sid}/entries').json() assert len([x for x in lst if x['route']=='/about/'])==1 build=client.post(f'/api/sites/{sid}/builds',json={}) assert build.status_code==200,build.text def test_attributes_bind_imported_and_typed_fields(client): sid=create(client,'automotive') product=client.post(f'/api/sites/{sid}/entries',json={'type':'product','title':'BYD SUV','route':'/product/byd-suv/','meta':{'fields':{'brand':'BYD','model':'SUV 1','specs':'Electric'}}}) assert product.status_code==201 eid=product.json()['id'] blocks=[{'id':'attributes-1','type':'attributes','title':'Technical Parameters'}] save=client.put(f'/api/sites/{sid}/pages/{eid}/blueprint',json={'blocks':blocks,'page_template':'product'}) assert save.status_code==200,save.text b=client.post(f'/api/sites/{sid}/builds',json={}) assert b.status_code==200,b.text page=client.get(f'/preview/{sid}/{b.json()["version"]}/product/byd-suv/').text assert 'Technical Parameters' in page and '>BYD<' in page and 'SUV 1' in page