import json import os from tests.legacy import LegacyTestClient as TestClient import pytest from app import db, enterprise, builder from app.main import app from app.worker import run_once @pytest.fixture def root(tmp_path,monkeypatch): monkeypatch.setattr(db,'DATA',tmp_path) monkeypatch.setattr(db,'DB_FILE',tmp_path/'app.sqlite3') monkeypatch.setenv('SITEFORGE_ALLOW_LEGACY_DIRECT_DESIGN_WRITE','1') monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','administrator-strong-password-2026') monkeypatch.setenv('SITEFORGE_SESSION_SECRET','secure-random-test-key-with-long-length-1234') monkeypatch.setenv('SITEFORGE_PUBLISH_ROOT',str(tmp_path/'published')) with TestClient(app) as c: assert c.post('/api/auth/login',json={'username':'admin','password':'administrator-strong-password-2026'}).status_code==200 csrf=c.get('/api/auth/me').json()['csrf'] c.headers.update({'X-SiteForge-CSRF':csrf}) yield c def site(c,name): r=c.post('/api/sites',json={'name':name,'theme':'corporate','mode':'migration','domain':'https://example.org'}) assert r.status_code==201,r.text return r.json()['id'] def test_team_rbac_isolation(root): one=site(root,'Company A') two=site(root,'Company B') response=root.post('/api/enterprise/team/users',json={'username':'editor1','password':'long-editor-password-2026','display_name':'Editor'}) assert response.status_code==201,response.text uid=response.json()['id'] r=root.put(f'/api/enterprise/sites/{one}/members',json={'user_id':uid,'role':'editor'}) assert r.status_code==200,r.text root.post('/api/auth/logout') assert root.post('/api/auth/login',json={'username':'editor1','password':'long-editor-password-2026'}).status_code==200 root.headers.update({'X-SiteForge-CSRF':root.get('/api/auth/me').json()['csrf']}) visible=root.get('/api/sites').json() assert [x['id'] for x in visible]==[one] assert root.get(f'/api/sites/{two}').status_code==403 assert root.post(f'/api/sites/{two}/entries',json={'title':'Blocked'}).status_code==403 assert root.post('/api/sites',json={'name':'Blocked'}).status_code==403 assert root.post(f'/api/sites/{one}/entries',json={'title':'Allowed','type':'page','route':'/allowed/'}).status_code==201 assert root.post(f'/api/sites/{one}/builds/invalid/publish-local').status_code==403 assert root.get('/api/enterprise/team/users').status_code==403 assert root.get(f'/api/enterprise/sites/{one}/baseline/report').status_code==200 def test_baseline_and_release_gate(root): sid=site(root,'Legacy') x=root.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Original','route':'/legacy/a.html','seo_title':'Old Title'}) assert x.status_code==201 assert root.post(f'/api/enterprise/sites/{sid}/baseline/snapshot').json()['captured']==2 e=x.json()['id'] assert root.delete(f'/api/sites/{sid}/entries/{e}').status_code==200 report=root.get(f'/api/enterprise/sites/{sid}/baseline/report').json() assert report['errors']==1 build=root.post(f'/api/sites/{sid}/builds',json={}) assert build.status_code==200,build.text bid=build.json()['version'] publish=root.post(f'/api/sites/{sid}/builds/{bid}/publish-local') assert publish.status_code==409 assert root.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Replaced','route':'/legacy/a.html','seo_title':'Old Title'}).status_code==201 assert root.get(f'/api/enterprise/sites/{sid}/baseline/report').json()['errors']==0 rebuilt=root.post(f'/api/sites/{sid}/builds',json={}).json()['version'] publish=root.post(f'/api/sites/{sid}/builds/{rebuilt}/publish-local') assert publish.status_code==200,publish.text qa=root.get(f'/api/enterprise/sites/{sid}/builds/{rebuilt}/quality') assert qa.status_code==200,qa.text assert qa.json()['files']>=2 history=root.get(f'/api/enterprise/sites/{sid}/releases').json() assert history and history[0]['build_id']==rebuilt rollback=root.post(f'/api/enterprise/sites/{sid}/releases/{rebuilt}/rollback') assert rollback.status_code==200,rollback.text def test_revision_restore_and_design_tokens(root): sid=site(root,'Design') x=root.post(f'/api/sites/{sid}/entries',json={'type':'page','title':'Draft v1','route':'/old-page/'}) entry=x.json()['id'] assert root.patch(f'/api/sites/{sid}/entries/{entry}',json={'title':'Draft v2'}).status_code==200 revs=root.get(f'/api/enterprise/sites/{sid}/entries/{entry}/revisions').json() assert len(revs)>=1 old=root.get(f'/api/enterprise/sites/{sid}/revisions/{revs[0]["id"]}').json() assert old['entry']['title']=='Draft v1' assert root.post(f'/api/enterprise/sites/{sid}/revisions/{revs[0]["id"]}/restore').status_code==200 refreshed=root.get(f'/api/sites/{sid}/entries').json() assert any(i['title']=='Draft v1' for i in refreshed) invalid=root.put(f'/api/enterprise/sites/{sid}/design-tokens',json={'font':'system','radius':999}) assert invalid.status_code==400 payload={'font':'serif','heading_scale':1.2,'radius':18,'section_spacing':80,'container_width':1360,'button_style':'pill','header_style':'compact'} assert root.put(f'/api/enterprise/sites/{sid}/design-tokens',json=payload).status_code==200 assert root.get(f'/api/enterprise/sites/{sid}/design-tokens').json()==payload build=root.post(f'/api/sites/{sid}/builds',json={}).json()['version'] import pathlib css=(db.DATA/'builds'/sid/build/'dist'/'assets'/'custom.css').read_text() assert 'Georgia' in css and '--sf-width:1360px' in css def test_background_worker_and_audit(root): sid=site(root,'Jobs') res=root.post(f'/api/enterprise/sites/{sid}/jobs',json={'kind':'build'}) assert res.status_code==202,res.text assert run_once(executor=lambda sid,opts:builder.build(sid,opts.get('mirror_images',False)))==res.json()['id'] jobs=root.get(f'/api/enterprise/sites/{sid}/jobs').json() assert jobs[0]['state']=='done' audits=root.get('/api/enterprise/audit').json() assert any(x['operation']=='POST' for x in audits) assert root.get('/ui/enterprise-ui.js').status_code==200 def test_viewer_cannot_modify_or_see_other_site(root): own=site(root,'Site One'); foreign=site(root,'Site Two') user=root.post('/api/enterprise/team/users',json={'username':'viewer22','display_name':'Viewer','password':'long-viewer-password-2026'}).json()['id'] assert root.put(f'/api/enterprise/sites/{own}/members',json={'user_id':user,'role':'viewer'}).status_code==200 root.post('/api/auth/logout') root.post('/api/auth/login',json={'username':'viewer22','password':'long-viewer-password-2026'}) root.headers['X-SiteForge-CSRF']=root.get('/api/auth/me').json()['csrf'] assert root.get(f'/api/sites/{own}/entries').status_code==200 assert root.get(f'/preview/{foreign}/any/').status_code==403 assert root.post(f'/api/sites/{own}/builds',json={}).status_code==403 assert root.put(f'/api/enterprise/sites/{own}/design-tokens',json={}).status_code==403 assert root.get(f'/api/enterprise/sites/{own}/jobs').status_code==200 assert root.get(f'/api/enterprise/sites/{foreign}/jobs').status_code==403