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>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user