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>
110 lines
5.3 KiB
Python
110 lines
5.3 KiB
Python
"""New V6 async contracts: these tests do not use legacy sync adapters."""
|
|
import json
|
|
from fastapi.testclient import TestClient
|
|
import pytest
|
|
from app import db,builder,jobs,worker,enterprise
|
|
from app.main import app
|
|
|
|
@pytest.fixture
|
|
def web(tmp_path,monkeypatch):
|
|
monkeypatch.setattr(db,'DATA',tmp_path)
|
|
monkeypatch.setattr(db,'DB_FILE',tmp_path/'siteforge.sqlite3')
|
|
monkeypatch.setenv('SITEFORGE_ADMIN_PASSWORD','test-admin-password-for-queue')
|
|
monkeypatch.setenv('SITEFORGE_SESSION_SECRET','test-session-secret-for-queue-2026-much-longer')
|
|
with TestClient(app) as c:
|
|
assert c.post('/api/auth/login',json={'password':'test-admin-password-for-queue'}).status_code==200
|
|
c.headers['X-SiteForge-CSRF']=c.get('/api/auth/me').json()['csrf']
|
|
sid=c.post('/api/sites',json={'name':'Queued Site','theme':'corporate'}).json()['id']
|
|
yield c,sid
|
|
|
|
def test_api_enqueue_idempotency_cancel_and_version(web):
|
|
c,sid=web
|
|
from app.version import VERSION
|
|
assert c.get('/api/health').json()['version']==VERSION
|
|
key='same-request-unique-123'
|
|
r=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':key})
|
|
assert r.status_code==202,r.text
|
|
j=r.json()['id']
|
|
duplicate=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':False},headers={'Idempotency-Key':key})
|
|
assert duplicate.json()['id']==j and duplicate.json()['existing']
|
|
conflict=c.post(f'/api/sites/{sid}/builds',json={'mirror_images':True},headers={'Idempotency-Key':key})
|
|
assert conflict.status_code==409
|
|
cancelling=c.post(f'/api/enterprise/sites/{sid}/jobs/{j}/cancel')
|
|
assert cancelling.status_code==200 and cancelling.json()['state']=='cancelled'
|
|
assert worker.run_once(executor=lambda sid,options:builder.build(sid)) is None
|
|
assert c.get(f'/api/enterprise/sites/{sid}/jobs/{j}').json()['state']=='cancelled'
|
|
|
|
def test_only_worker_generates_static_build(web):
|
|
c,sid=web
|
|
start=c.get(f'/api/sites/{sid}/builds').json()
|
|
submitted=c.post(f'/api/sites/{sid}/builds',json={})
|
|
assert submitted.status_code==202
|
|
assert c.get(f'/api/sites/{sid}/builds').json()==start
|
|
job=submitted.json()['id']
|
|
assert worker.run_once(executor=lambda sid,opts:builder.build(sid,bool(opts.get('mirror_images',False))))==job
|
|
record=c.get(f'/api/enterprise/sites/{sid}/jobs/{job}').json()
|
|
assert record['state']=='done',record['error']
|
|
version=json.loads(record['result_json'])['version']
|
|
assert c.get(f'/preview/{sid}/{version}/').status_code==200
|
|
|
|
def test_failed_job_retries_and_lease_expiry(web):
|
|
c,sid=web
|
|
job=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
|
attempts=[]
|
|
def fail_once(sid,opts):
|
|
attempts.append(sid)
|
|
if len(attempts)==1:raise TimeoutError('transient timeout')
|
|
return {'version':'ok','pages':1}
|
|
worker.run_once(executor=fail_once)
|
|
row=db.one('SELECT * FROM build_jobs WHERE id=?',(job,))
|
|
assert row['state']=='retrying' and row['attempts']==1
|
|
db.execute('UPDATE build_jobs SET available_at=? WHERE id=?',(db.now(),job))
|
|
worker.run_once(executor=fail_once)
|
|
assert db.one('SELECT state,attempts FROM build_jobs WHERE id=?',(job,))=={'state':'done','attempts':2}
|
|
second=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
|
db.execute("UPDATE build_jobs SET state='running',worker_id='lost',lease_until='2020-01-01T00:00:00+00:00' WHERE id=?",(second,))
|
|
assert jobs.recover_expired()==1
|
|
assert db.one('SELECT state FROM build_jobs WHERE id=?',(second,))['state']=='retrying'
|
|
|
|
def test_real_subprocess_worker(web):
|
|
c,sid=web
|
|
jid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
|
worker.run_once() # real Python subprocess, reads same test SQLite database
|
|
row=db.one('SELECT * FROM build_jobs WHERE id=?',(jid,))
|
|
assert row['state']=='done',row['error']
|
|
report=json.loads(row['result_json'])
|
|
assert report['pages']>=1
|
|
|
|
def test_cancel_actively_running_subprocess(web,monkeypatch):
|
|
import subprocess,sys,threading,time
|
|
c,sid=web
|
|
jobid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
|
original=subprocess.Popen
|
|
monkeypatch.setattr(worker.subprocess,'Popen',lambda *args,**kwargs:original(
|
|
[sys.executable,'-c','import time;time.sleep(10)'],**kwargs))
|
|
t=threading.Thread(target=worker.run_once)
|
|
t.start()
|
|
for _ in range(100):
|
|
row=db.one('SELECT state FROM build_jobs WHERE id=?',(jobid,))
|
|
if row and row['state']=='running':break
|
|
time.sleep(0.02)
|
|
r=c.post(f'/api/enterprise/sites/{sid}/jobs/{jobid}/cancel')
|
|
assert r.status_code==200
|
|
t.join(timeout=6)
|
|
assert not t.is_alive()
|
|
assert db.one('SELECT state FROM build_jobs WHERE id=?',(jobid,))['state']=='cancelled'
|
|
|
|
|
|
def test_build_timeout_sets_retrying(web,monkeypatch):
|
|
import subprocess,sys
|
|
c,sid=web
|
|
jobid=c.post(f'/api/sites/{sid}/builds',json={}).json()['id']
|
|
db.execute('UPDATE build_jobs SET timeout_seconds=1 WHERE id=?',(jobid,))
|
|
original=subprocess.Popen
|
|
monkeypatch.setattr(worker.subprocess,'Popen',lambda *args,**kwargs:original(
|
|
[sys.executable,'-c','import time;time.sleep(10)'],**kwargs))
|
|
worker.run_once()
|
|
row=db.one('SELECT state,error FROM build_jobs WHERE id=?',(jobid,))
|
|
assert row['state']=='retrying',row
|
|
assert 'timed out' in row['error']
|