o2o-site-AEO/solution/backend/tests/test_rollback.py
Mina Choi f2087aad5e feat(solution): 발행 워커·버전 관리와 예약·미리보기 정리
상시 프리렌더와 중복 예약 안내를 없애고, 검수된 발행 버전을 보존한다. 미리보기는 실제 렌더 완료까지 스피너를 표시한다.

사이트 81건, 발행·롤백·서치콘솔 45건, 프로세스 수명 3건 통과. 빌더·사이트 빌드 및 compose 설정 검증 통과.
2026-09-15 16:12:16 +09:00

132 lines
6.4 KiB
Python

"""롤백 — 예전 버전으로 공개 주소를 되돌린다.
★ 이 경로가 절대 하면 안 되는 것:
- 재수집·재생성으로 스냅샷을 새로 만드는 것 (그 버전이 발행됐던 그대로여야 한다)
- 실패했는데 site.current_version_id 를 바꾸는 것 (직전 공개본이 그대로 있어야 한다)
- 지금 공개 중인 버전으로 "롤백"을 허용하는 것 (의미 없는 잡을 만든다)
"""
import uuid
from common.enums import BuildStatus, JobStatus, SiteStatus
from crud.job_crud import JobQueue
from worker.handlers import build_handler
from worker.runner import Worker
from tests.test_build_publish import _approved_media, _place, _verified_facts
async def _run():
assert await Worker("w", JobQueue(), build_handler(), job_deadline_sec=60).process_one() is True
async def _publish(client, h, pid, intro="하조대 해변 도보 3분 거리의 펜션입니다."):
await _verified_facts(client, h, pid, {"intro": intro})
job_id = (await client.post(f"/v1/place/{pid}/site/build", headers=h, json={"publish": True})).json()["job_id"]
await _run()
return (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]["result"]
async def test_rollback_to_earlier_version(auth_headers, client, db_engine):
"""검증: v1 발행 → v2 발행 → v1 로 롤백.
기대결과: 공개 버전이 다시 v1 이 된다. 롤백은 새 site_versions 행을 만들지 않는다."""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid, "v1 소개 — 하조대 해변 도보 3분 거리의 펜션입니다.")
assert r1["published"] is True
r2 = await _publish(client, h, pid, "v2 소개 — 리모델링을 마친 하조대 앞 오션뷰 펜션입니다.")
assert r2["published"] is True
assert r2["version"] == r1["version"] + 1
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] == JobStatus.DONE.value, job.get("last_error")
result = job["result"]
assert result["rolled_back"] is True, result.get("error")
assert result["target_version"] == r1["version"]
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["site"]["status"] == SiteStatus.PUBLISHED.value
assert site["current_version"]["version"] == r1["version"]
assert site["current_version"]["build_status"] == BuildStatus.BUILT.value
versions = (await client.get(f"/v1/place/{pid}/site/version/list", headers=h)).json()["versions"]
# ★ 롤백은 새 버전을 만들지 않는다 — v1·v2 두 개뿐이어야 한다.
assert len(versions) == 2
async def test_rollback_rejects_current_version(auth_headers, client, db_engine):
"""검증: 지금 공개 중인 버전으로 롤백을 시도한다.
기대결과: ★ 성공으로 끝나지 않는다(last_error) — 의미 없는 재굽기를 하지 않는다.
(BuildAborted 와 같은 패턴 — 재시도해도 소용없는 중단이라 PENDING(백오프)으로 남고,
소진되면 DEAD 된다. 여기서 보는 건 "그래도 site 상태는 안 바뀐다"는 것이다.)"""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] != JobStatus.DONE.value
assert "이미" in (job.get("last_error") or "")
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["current_version"]["version"] == r1["version"]
async def test_rollback_rejects_unknown_version(auth_headers, client, db_engine):
"""검증: 존재하지 않는 버전 번호로 롤백을 시도한다.
기대결과: ★ 잡이 성공으로 끝나지 않는다 — 공개 상태는 그대로다."""
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": 999}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] != JobStatus.DONE.value
site = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site["site"]["status"] == SiteStatus.PUBLISHED.value
assert site["current_version"]["version"] == r1["version"]
async def test_failed_rollback_keeps_current_published_version(auth_headers, client, db_engine, monkeypatch):
"""검증: 롤백 대상 렌더가 실패한다(구조화 데이터 불일치로 시뮬레이션).
기대결과: ★ site.current_version_id 는 바뀌지 않는다 — 직전 공개본이 그대로 서비스된다."""
from services import render_service
h = await auth_headers("u1")
pid = await _place(client, h)
await _approved_media(db_engine, pid)
r1 = await _publish(client, h, pid, "v1 소개 — 하조대 해변 도보 3분 거리의 펜션입니다.")
r2 = await _publish(client, h, pid, "v2 소개 — 리모델링을 마친 하조대 앞 오션뷰 펜션입니다.")
site_before = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site_before["current_version"]["version"] == r2["version"]
async def _broken(payload_path, site_version, timeout_sec):
raise render_service.RenderFailed("렌더러가 죽었다(시뮬레이션)")
monkeypatch.setattr(render_service, "render_site", _broken)
job_id = (await client.post(
f"/v1/place/{pid}/site/version/rollback", headers=h, json={"target_version": r1["version"]}
)).json()["job_id"]
await _run()
job = (await client.get(f"/v1/job/{job_id}", headers=h)).json()["job"]
assert job["status"] == JobStatus.DONE.value
assert job["result"]["rolled_back"] is False
site_after = (await client.get(f"/v1/place/{pid}/site", headers=h)).json()
assert site_after["current_version"]["version"] == r2["version"], "실패했는데 공개 버전이 바뀌었다"