"""Regression tests for release safety and CMS review persistence.""" import json from fastapi.testclient import TestClient from fastapi import HTTPException import pytest from app import db, cms_pipeline, release_api from app.main import app def test_pending_and_failed_build_cannot_be_deployed(monkeypatch,tmp_path): monkeypatch.setattr(db,'DATA',tmp_path) monkeypatch.setattr(db,'DB_FILE',tmp_path/'pending.sqlite3') db.init() with db.connect() as c: c.execute('INSERT INTO sites(id,name,created_at,updated_at) VALUES (?,?,?,?)',('sitepending','Sample',db.now(),db.now())) for state in ['queued','running','failed','unknown']: c.execute('INSERT INTO builds(id,site_id,created_at,status,zip_path) VALUES (?,?,?,?,?)', (state,'sitepending',db.now(),state,str(tmp_path/'missing.zip'))) for state in ['queued','running','failed','unknown']: with pytest.raises(HTTPException) as exc: release_api.approved('sitepending',state) assert exc.value.status_code==409 def test_cms_warnings_saved_and_reviewed(monkeypatch,tmp_path): monkeypatch.setattr(db,'DATA',tmp_path) monkeypatch.setattr(db,'DB_FILE',tmp_path/'review.sqlite3') monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','test-admin-password-for-cms-reviews') monkeypatch.setenv('SITEFORGE_SESSION_SECRET','test-session-secret-for-cms-reviews-long') with TestClient(app) as c: assert c.post('/api/auth/login',json={'password':'test-admin-password-for-cms-reviews'}).status_code==200 c.headers['X-SiteForge-CSRF']=c.get('/api/auth/me').json()['csrf'] res=c.post('/api/sites',json={'name':'CMS Review Test','mode':'migration'}) assert res.status_code==201,res.text sid=res.json()['id'] blob={'items':[{'id':3,'type':'page','title':{'rendered':'About'},'link':'https://old.example/about/', 'content':{'rendered':'

old

'},'acf':{'related':[1,2]}}]} res=c.post(f'/api/migrations/sites/{sid}/import',data={'platform':'wordpress-rest','conflict':'skip'}, files={'file':('export.json',json.dumps(blob).encode(),'application/json')}) assert res.status_code==200,res.text assert res.json()['pending_review_count']==1 get=c.get(f'/api/migrations/sites/{sid}/reviews') assert get.status_code==200 and get.json()['pending']==1 issue=get.json()['items'][0]['id'] reject=c.post(f'/api/migrations/sites/{sid}/reviews/{issue}/resolve',json={'resolution':'ok'}) assert reject.status_code==400 ok=c.post(f'/api/migrations/sites/{sid}/reviews/{issue}/resolve',json={'resolution':'已逐项检查关联产品及图片,确认无需迁移旧关联。'}) assert ok.status_code==200,ok.text assert ok.json()['pending']==0 assert c.get(f'/api/migrations/sites/{sid}/reviews').json()['items']==[] assert c.get(f'/api/migrations/sites/{sid}/reviews?state=resolved').json()['items'][0]['resolution'] def test_signatures_are_bound_to_http_path(monkeypatch,tmp_path): from app import deploy_agent import zipfile,io,time from fastapi.testclient import TestClient monkeypatch.setattr(deploy_agent,'ROOT',tmp_path) secret='release-secret-very-long-at-least-thirtytwo-bytes' monkeypatch.setenv('SITEFORGE_DEPLOY_SECRET',secret) buf=io.BytesIO() with zipfile.ZipFile(buf,'w') as z:z.writestr('index.html','OK') client=TestClient(deploy_agent.app) stamp=str(int(time.time())) headers={'X-SiteForge-Timestamp':stamp,'X-SiteForge-Signature':deploy_agent.signature(secret,'company',stamp,buf.getvalue(),'POST','/deploy/company')} created=client.post('/deploy/company',headers=headers,content=buf.getvalue()) assert created.status_code==200 version=created.json()['release'] # A read-only inventory signature must not authorize state-changing rollback. inventory_headers={'X-SiteForge-Timestamp':stamp,'X-SiteForge-Signature':deploy_agent.signature(secret,'company',stamp,b'','GET','/sites/company/releases')} assert client.get('/sites/company/releases',headers=inventory_headers).status_code==200 assert client.post(f'/sites/company/rollback/{version}',headers=inventory_headers).status_code==401 rollback_headers={'X-SiteForge-Timestamp':stamp,'X-SiteForge-Signature':deploy_agent.signature(secret,'company',stamp,b'','POST',f'/sites/company/rollback/{version}')} assert client.post(f'/sites/company/rollback/{version}',headers=rollback_headers).status_code==200 def test_art_installer_idempotent_mock_checkout(tmp_path): from scripts.install_into_art_design_pro import install base=tmp_path/'official'/'src'/'router'/'routes' base.mkdir(parents=True) (base/'asyncRoutes.ts').write_text('export const asyncRoutes: MenuListType[] = [];',encoding='utf-8') (tmp_path/'official'/'package.json').write_text('{"name":"art-design-pro"}',encoding='utf-8') install(tmp_path/'official',dry_run=True) assert 'SITEFORGE_FACTORY' not in (base/'asyncRoutes.ts').read_text() install(tmp_path/'official',dry_run=False) before=(base/'asyncRoutes.ts').read_text() assert before.count('// SITEFORGE_FACTORY_V7_REGISTERED')==1 assert 'SiteForgeMigration' in before install(tmp_path/'official',dry_run=False) assert (base/'asyncRoutes.ts').read_text()==before def test_new_site_missing_content_gets_noindex_scaffold(monkeypatch,tmp_path): from app import builder,quality from fastapi.testclient import TestClient monkeypatch.setattr(db,'DATA',tmp_path) monkeypatch.setattr(db,'DB_FILE',tmp_path/'scaffold.sqlite3') monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','test-admin-password-for-scaffold') monkeypatch.setenv('SITEFORGE_SESSION_SECRET','test-session-secret-for-scaffold-extended') with TestClient(app) as c: assert c.post('/api/auth/login',json={'password':'test-admin-password-for-scaffold'}).status_code==200 c.headers['X-SiteForge-CSRF']=c.get('/api/auth/me').json()['csrf'] sid=c.post('/api/sites',json={'name':'New Company','theme':'automotive'}).json()['id'] version=builder.build(sid)['version'] root=tmp_path/'builds'/sid/version/'dist' assert (root/'products/index.html').is_file() assert (root/'brands/index.html').is_file() assert 'noindex' in (root/'brands/index.html').read_text(encoding='utf-8') assert quality.report(sid,version)['errors']==0