Files
cms-SiteForge/tests/test_p1_platform.py
浅念 8cea7dee20 chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests)
- Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR
  imports keep post/product types instead of collapsing to page
- Docs: trade-pro theme port spec (R3) and implementation plan

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

121 lines
5.9 KiB
Python

"""Deterministic P1 tests: no actual network or external infrastructure."""
import socket
from app import safe_http, secret_refs
from fastapi.testclient import TestClient
from app import db
from app.main import app
import pytest
def test_public_ip_pinned_and_bounded(monkeypatch):
seen=[]
def dns(host, port, **kwargs):
return [(socket.AF_INET,socket.SOCK_STREAM,6,'',('93.184.216.34',port))]
monkeypatch.setattr(safe_http.socket,'getaddrinfo',dns)
class Response:
status=200
def getheaders(self):return [('Content-Type','text/html')]
def read(self,n):return b'hello'
class FakeConnection:
def __init__(self,host,ip,port,timeout):seen.append((host,ip,port,timeout))
def request(self,method,url,headers,body=None):seen.append((method,url))
def getresponse(self):return Response()
def close(self):pass
monkeypatch.setattr(safe_http,'_PinnedHTTPS',FakeConnection)
r=safe_http.fetch('https://example.org/products/?page=1',max_bytes=8)
assert r.content==b'hello'
assert seen[0]==('example.org','93.184.216.34',443,12)
assert seen[1]==('GET','/products/?page=1')
def test_public_only_dns_and_url_restrictions(monkeypatch):
def dns(host,port,**kwargs):
ip={'rebinding.example':'10.0.0.5','mixed.example':'93.184.216.34'}.get(host,'93.184.216.34')
r=[(socket.AF_INET,socket.SOCK_STREAM,6,'',(ip,port))]
if host=='mixed.example':r.append((socket.AF_INET,socket.SOCK_STREAM,6,'',('127.0.0.1',port)))
return r
monkeypatch.setattr(safe_http.socket,'getaddrinfo',dns)
for url in ['http://localhost/','http://169.254.169.254/latest/meta-data/',
'https://rebinding.example/','https://mixed.example/',
'file:///etc/passwd','http://user:pass@example.org/',
'https://example.org:8080/']:
with pytest.raises(safe_http.OutboundError):safe_http.resolve_target(url)
assert safe_http.resolve_target('https://example.org/')[3]=='93.184.216.34'
def test_redirect_to_private_host_is_not_followed(monkeypatch):
def dns(host,port,**kwargs):
ip='10.0.0.2' if host=='internal.example' else '93.184.216.34'
return [(socket.AF_INET,socket.SOCK_STREAM,6,'',(ip,port))]
monkeypatch.setattr(safe_http.socket,'getaddrinfo',dns)
class Response:
status=302
def getheaders(self):return [('Location','http://internal.example/secret')]
def read(self,n):return b''
class FakeConnection:
def __init__(self,*args):pass
def request(self,*args,**kwargs):pass
def getresponse(self):return Response()
def close(self):pass
monkeypatch.setattr(safe_http,'_PinnedHTTPS',FakeConnection)
with pytest.raises(safe_http.OutboundError):safe_http.fetch('https://example.org/',redirects=3)
def test_secret_refs_deny_literals_and_path_traversal(monkeypatch):
monkeypatch.setenv('SITEFORGE_UNIT_TOKEN','sample-secret')
assert secret_refs.resolve('env:SITEFORGE_UNIT_TOKEN')=='sample-secret'
for ref in ['my-plain-password','env:PATH','file:../root','file:/etc/passwd','env:SITEFORGE_NOT_SET']:
with pytest.raises(secret_refs.SecretReferenceError):secret_refs.resolve(ref)
def test_queue_limits_and_repeated_requests(monkeypatch,tmp_path):
monkeypatch.setattr(db,'DATA',tmp_path)
monkeypatch.setattr(db,'DB_FILE',tmp_path/'p1.sqlite3')
monkeypatch.setenv('SITEFORGE_MAX_PENDING_JOBS_PER_SITE','1')
monkeypatch.setenv('SITEFORGE_MAX_PENDING_JOBS','10')
monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','p1-test-password-with-strong-entropy')
monkeypatch.setenv('SITEFORGE_SESSION_SECRET','p1-test-secret-with-enough-entropy-1234567890')
with TestClient(app) as client:
assert client.post('/api/auth/login',json={'password':'p1-test-password-with-strong-entropy'}).status_code==200
client.headers['X-SiteForge-CSRF']=client.get('/api/auth/me').json()['csrf']
sid=client.post('/api/sites',json={'name':'Queue capacity'}).json()['id']
one=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False})
assert one.status_code==202
duplicated=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':False})
assert duplicated.status_code==202 and duplicated.json()['existing']
over=client.post(f'/api/sites/{sid}/builds',json={'mirror_images':True})
assert over.status_code==429
health=client.get('/api/health')
assert health.status_code==200 and len(health.headers['X-Request-ID'])==16
def test_schema_ledger_idempotent_and_checksum_guard(monkeypatch,tmp_path):
monkeypatch.setattr(db,'DATA',tmp_path)
monkeypatch.setattr(db,'DB_FILE',tmp_path/'schema.sqlite3')
db.init()
db.init()
assert db.one('SELECT COUNT(*) AS n FROM schema_migrations')['n']==1
assert db.one("SELECT name FROM sqlite_master WHERE name='platform_limit_overrides'")
db.execute("UPDATE schema_migrations SET checksum='corrupted'")
with pytest.raises(RuntimeError,match='checksum mismatch'):db.init()
def test_post_uses_pinned_ip_and_never_redirects(monkeypatch):
monkeypatch.setattr(safe_http.socket,'getaddrinfo',lambda host,port,**k:[
(socket.AF_INET,socket.SOCK_STREAM,6,'',('93.184.216.34',port))])
seen=[]
class Response:
status=201
def getheaders(self):return [('Content-Type','application/json')]
def read(self,n):return b'{"release":"xyz"}'
class Connection:
def __init__(self,host,ip,port,timeout):seen.append((host,ip))
def request(self,method,path,body=None,headers=None):seen.append((method,path,body))
def getresponse(self):return Response()
def close(self):pass
monkeypatch.setattr(safe_http,'_PinnedHTTPS',Connection)
result=safe_http.post('https://releases.example.org/deploy/site1',body=b'zip-data')
assert result.status_code==201 and result.json()['release']=='xyz'
assert seen[0]==('releases.example.org','93.184.216.34')
assert seen[1]==('POST','/deploy/site1',b'zip-data')