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