Files
cms-SiteForge/tests/test_enterprise.py
T
浅念 3c57c167f2 chore: import SiteForge Factory V5 source (baseline)
导入 SiteForge Factory V5 基线源码:
- FastAPI + SQLite + Docker Compose(Web + Worker)架构
- 四套商业主题 × 14 页面模板(1.1.0,含历史版本快照)
- 四种建站模式骨架 + 迁移审计 + 发布门禁
- 52 项 pytest 测试
- 补全计划主文档 docs/superpowers/plans/

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 10:42:19 +08:00

135 lines
7.5 KiB
Python

import json
import os
from fastapi.testclient import TestClient
import pytest
from app import db, enterprise, worker
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_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(encoding='utf-8')
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()==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_background_worker_retries_transient_failure(root,monkeypatch):
sid=site(root,'Retry Jobs')
job=root.post(f'/api/enterprise/sites/{sid}/jobs',json={'kind':'build'}).json()['id']
monkeypatch.setenv('SITEFORGE_JOB_MAX_ATTEMPTS','3')
monkeypatch.setattr(worker.builder,'build',lambda *_: (_ for _ in ()).throw(RuntimeError('temporary failure')))
assert worker.run_once()==job
queued=db.one('SELECT state,attempts,error,retry_at FROM build_jobs WHERE id=?',(job,))
assert queued['state']=='queued' and queued['attempts']==1
assert queued['retry_at'] and 'temporary failure' in queued['error']
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