"""롤백 — 예전 버전으로 공개 주소를 되돌린다. ★ 이 경로가 절대 하면 안 되는 것: - 재수집·재생성으로 스냅샷을 새로 만드는 것 (그 버전이 발행됐던 그대로여야 한다) - 실패했는데 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"], "실패했는데 공개 버전이 바뀌었다"