121 lines
5.6 KiB
Python
121 lines
5.6 KiB
Python
|
|
"""V8.19 unified console contract; server permissions remain authoritative."""
|
||
|
|
from app.version import VERSION
|
||
|
|
import json
|
||
|
|
import pytest
|
||
|
|
from tests.test_enterprise import root,site
|
||
|
|
from scripts.install_into_art_design_pro import install, transform_routes
|
||
|
|
|
||
|
|
|
||
|
|
def test_bootstrap_admin_uses_one_navigation_and_no_secrets(root):
|
||
|
|
one=site(root,'HESU A');two=site(root,'HESU B')
|
||
|
|
r=root.get('/api/console/bootstrap')
|
||
|
|
assert r.status_code==200,r.text
|
||
|
|
info=r.json()
|
||
|
|
assert info['version']==VERSION
|
||
|
|
assert {x['id'] for x in info['sites']}=={one,two}
|
||
|
|
assert info['selected_site_id'] in {one,two}
|
||
|
|
assert info['capabilities']['create_site'] and info['capabilities']['deploy']
|
||
|
|
assert [i['key'] for i in info['navigation']]==['dashboard','sites','content','design','migration','release','system']
|
||
|
|
assert 'csrf' not in json.dumps(info).lower()
|
||
|
|
assert root.get('/api/console/bootstrap?site_id=not-a-site').status_code==403
|
||
|
|
|
||
|
|
|
||
|
|
def test_scope_does_not_disclose_foreign_site_and_rejects_unassigned(root):
|
||
|
|
one=site(root,'Assigned');two=site(root,'Not assigned')
|
||
|
|
uid=root.post('/api/enterprise/team/users',json={
|
||
|
|
'username':'v819view','display_name':'Viewer','password':'viewer-strong-password-2026'}).json()['id']
|
||
|
|
assert root.put(f'/api/enterprise/sites/{one}/members',json={'user_id':uid,'role':'viewer'}).status_code==200
|
||
|
|
root.post('/api/auth/logout')
|
||
|
|
assert root.post('/api/auth/login',json={'username':'v819view','password':'viewer-strong-password-2026'}).status_code==200
|
||
|
|
root.headers['X-SiteForge-CSRF']=root.get('/api/auth/me').json()['csrf']
|
||
|
|
r=root.get('/api/console/bootstrap')
|
||
|
|
assert r.status_code==200,r.text
|
||
|
|
b=r.json()
|
||
|
|
assert [s['id'] for s in b['sites']]==[one]
|
||
|
|
assert b['capabilities']['role']=='viewer'
|
||
|
|
assert not b['capabilities']['edit'] and not b['capabilities']['deploy']
|
||
|
|
assert not b['capabilities']['manage_users']
|
||
|
|
assert all(item['key']!='system' for item in b['navigation'])
|
||
|
|
assert root.get(f'/api/console/bootstrap?site_id={two}').status_code==403
|
||
|
|
assert root.get(f'/api/sites/{two}/entries').status_code==403
|
||
|
|
assert root.get(f'/api/console/bootstrap?site_id={one}').status_code==200
|
||
|
|
|
||
|
|
|
||
|
|
def test_empty_site_roles_still_have_dashboard(root):
|
||
|
|
resp=root.get('/api/console/bootstrap')
|
||
|
|
assert resp.status_code==200
|
||
|
|
nav=resp.json()['navigation']
|
||
|
|
assert nav[0]['key']=='dashboard'
|
||
|
|
assert all(n['key'] not in {'content','design','migration','release'} for n in nav)
|
||
|
|
|
||
|
|
|
||
|
|
def fixture(tmp_path):
|
||
|
|
repo=tmp_path/'art';route=repo/'src/router/routes/asyncRoutes.ts'
|
||
|
|
route.parent.mkdir(parents=True,exist_ok=True)
|
||
|
|
route.write_text('export const asyncRoutes: MenuListType[] = [\n{path:"/unrelated",name:"Safe"},\n];\n')
|
||
|
|
(repo/'package.json').write_text('{"name":"art-design-pro"}')
|
||
|
|
return repo,route
|
||
|
|
|
||
|
|
|
||
|
|
def test_art_installer_single_menu_and_safe_backup(tmp_path):
|
||
|
|
repo,route=fixture(tmp_path)
|
||
|
|
original=route.read_text()
|
||
|
|
result=install(repo,dry_run=True)
|
||
|
|
assert result['dry_run'] and not result['conflicting_views']
|
||
|
|
assert route.read_text()==original
|
||
|
|
install(repo,dry_run=False)
|
||
|
|
updated=route.read_text()
|
||
|
|
assert updated.count("path: '/siteforge'")==1
|
||
|
|
assert "path: 'production'" in updated
|
||
|
|
assert updated.count('// SITEFORGE_FACTORY_V8_REGISTERED')==1
|
||
|
|
assert 'Safe' in updated
|
||
|
|
assert install(repo,verify=True)['installed']
|
||
|
|
assert route.with_name('asyncRoutes.ts.siteforge-backup').read_text()==original
|
||
|
|
install(repo,dry_run=False)
|
||
|
|
assert route.read_text()==updated
|
||
|
|
|
||
|
|
|
||
|
|
def test_installer_fails_closed_on_conflicting_views(tmp_path):
|
||
|
|
repo,route=fixture(tmp_path)
|
||
|
|
target=repo/'src/views/siteforge/ConsoleHome.vue'
|
||
|
|
target.parent.mkdir(parents=True)
|
||
|
|
target.write_text('someone else code')
|
||
|
|
with pytest.raises(ValueError,match='拒绝覆盖'):
|
||
|
|
install(repo,dry_run=False)
|
||
|
|
assert 'SITEFORGE' not in route.read_text()
|
||
|
|
install(repo,dry_run=False,overwrite_views=True)
|
||
|
|
assert target.with_name('ConsoleHome.vue.siteforge-backup').read_text()=='someone else code'
|
||
|
|
target.write_text('broken')
|
||
|
|
with pytest.raises(ValueError,match='不匹配'):
|
||
|
|
install(repo,verify=True)
|
||
|
|
|
||
|
|
|
||
|
|
def test_installer_rejects_partial_routes(tmp_path):
|
||
|
|
repo,route=fixture(tmp_path)
|
||
|
|
route.write_text('export const asyncRoutes = [ /* SITEFORGE_V819_ROUTES_BEGIN */ ]')
|
||
|
|
with pytest.raises(ValueError,match='标记不完整'):
|
||
|
|
install(repo,dry_run=False)
|
||
|
|
|
||
|
|
|
||
|
|
def test_legacy_upgrade_requires_explicit_confirmation(tmp_path):
|
||
|
|
repo,route=fixture(tmp_path)
|
||
|
|
route.write_text('export const asyncRoutes: any[] = [\n // SITEFORGE_FACTORY_V8_REGISTERED\n {path:"/siteforge/production",meta:{a:1}},\n // SITEFORGE_FACTORY_V7_REGISTERED\n {path:"/siteforge",children:[{path:"factory",name:"A"}]},\n {path:"/unrelated"},\n]')
|
||
|
|
with pytest.raises(ValueError,match='--migrate-legacy'):
|
||
|
|
install(repo,dry_run=False)
|
||
|
|
install(repo,dry_run=False,migrate_legacy=True)
|
||
|
|
routes=route.read_text()
|
||
|
|
assert routes.count("path: '/siteforge'")==1
|
||
|
|
assert 'unrelated' in routes
|
||
|
|
assert routes.count('// SITEFORGE_FACTORY_V8_REGISTERED')==1
|
||
|
|
|
||
|
|
|
||
|
|
def test_shared_sdk_and_vue_pages_exist():
|
||
|
|
from pathlib import Path
|
||
|
|
base=Path(__file__).resolve().parents[1]/'integrations/art-design-pro/src/views/siteforge'
|
||
|
|
assert (base/'_shared.ts').is_file()
|
||
|
|
assert 'X-SiteForge-CSRF' in (base/'_shared.ts').read_text()
|
||
|
|
for name in ('ConsoleHome.vue','ContentCenter.vue'):
|
||
|
|
text=(base/name).read_text()
|
||
|
|
assert '<template>' in text and "from './_shared'" in text
|
||
|
|
assert 'POST' in (base/'ContentCenter.vue').read_text() or 'postJson' in (base/'ContentCenter.vue').read_text()
|