8cea7dee20
- 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>
121 lines
5.9 KiB
Python
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')
|