diff --git a/solution/backend/crud/social_crud.py b/solution/backend/crud/social_crud.py index 089c726..fd8605b 100644 --- a/solution/backend/crud/social_crud.py +++ b/solution/backend/crud/social_crud.py @@ -4,6 +4,7 @@ import json from sqlalchemy import text from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import place_social_posts as Post +from common.enums import JobType async def transaction(fn): @@ -41,7 +42,7 @@ async def decide(s, post_id, sha, approve, via): ) ).first() if row and approve and row.account_id: - await enqueue(s, post_id, 9) + await enqueue(s, post_id, JobType.SOCIAL_POST.value) return bool(row) diff --git a/solution/backend/services/social_service.py b/solution/backend/services/social_service.py index 2be8334..cc78abb 100644 --- a/solution/backend/services/social_service.py +++ b/solution/backend/services/social_service.py @@ -19,7 +19,7 @@ from common.database.model.models import ( place_facts, owner_social_accounts as Account, ) -from common.enums import SiteStatus, ErrorType, PUBLISHABLE_FACT_STATUSES, SocialProvider +from common.enums import SiteStatus, ErrorType, JobType, PUBLISHABLE_FACT_STATUSES, SocialProvider from crud.place_crud import PlaceCRUD from crud import social_crud as db from services import site_payload, social_account_service as accounts @@ -238,7 +238,7 @@ async def create_draft(user_id, place_id, provider=2): await s.flush() row_id = row.post_id if row_id: - await db.enqueue(s, row.post_id, 8) + await db.enqueue(s, row.post_id, JobType.SOCIAL_DRAFT.value) return public_post(row) return await db.transaction(run) @@ -619,7 +619,7 @@ async def publish_reused_text(user_id, place_id, body: str): ) ).scalar_one_or_none() if row_id: - await db.enqueue(s, row_id, 9) + await db.enqueue(s, row_id, JobType.SOCIAL_POST.value) return row_id try: diff --git a/solution/backend/tests/test_social.py b/solution/backend/tests/test_social.py index 5d6a27b..b94a3a5 100644 --- a/solution/backend/tests/test_social.py +++ b/solution/backend/tests/test_social.py @@ -87,10 +87,76 @@ async def test_draft_dedup_owner_scope(client, auth_headers, db_engine): ) async with db_engine.begin() as c: assert ( - await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=8")) + await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=9")) ).scalar_one() == 1 +async def test_social_job_types_dispatch_to_the_right_handler(): + """실측(2026-09-22): 큐에 넣는 쪽(social_crud.decide, social_service.create_draft/ + publish_reused_text)은 job_type 숫자를 하드코딩(8/9)하고, 디스패처(worker/handlers.py)는 + JobType.SOCIAL_DRAFT(9)/SOCIAL_POST(10) enum 값으로 등록돼 있었다. 번호가 어긋나 있어서 + "쓰레드에 게시" 잡이 run_draft 로, "초안 생성" 잡이 run_rollback 으로 잘못 배달됐다 — + 잡은 에러 없이 DONE 으로 끝나지만 아무 일도 안 일어나는 조용한 실패였다. 큐 삽입 값만 + 보던 기존 테스트들(job_type=N 카운트)은 그 N 이 실제 핸들러와 맞는지는 확인하지 + 않아서 이 어긋남을 못 잡았다. 여기서는 워커가 실제로 쓰는 배달 경로 + (worker.handlers.HANDLERS)가 큐 삽입 쪽이 쓰는 것과 같은 JobType enum 값을 가리키는지 + 직접 대조한다 — 숫자가 다시 어긋나면(둘 중 하나가 하드코딩으로 되돌아가면) 여기서 잡힌다.""" + from common.enums import JobType + from services.social_service import run_draft, run_post + from worker.handlers import HANDLERS + + assert HANDLERS[JobType.SOCIAL_DRAFT.value] is run_draft + assert HANDLERS[JobType.SOCIAL_POST.value] is run_post + + +async def test_draft_and_post_jobs_enqueue_with_dispatchable_job_types( + client, auth_headers, db_engine, monkeypatch +): + """create_draft 가 넣는 job_type 이 실제로 run_draft 로, decide 가 넣는 job_type 이 + 실제로 run_post 로 배달되는지 엔드투엔드로 확인한다(위 테스트의 정적 대조를 실제 + 큐 삽입 값으로 한 번 더 검증). 같은 사업장에 site_version 을 새로 하나 더 발급해 + (place_id, site_version_id) 유니크 인덱스와 안 부딪히게 한다 — seed() 를 두 번 부르면 + 도메인('social-stay') 유니크 인덱스와 부딪힌다.""" + from worker.handlers import HANDLERS + + monkeypatch.setattr(service, "posting_enabled", lambda: True) + + h, pid, uid, v1 = await seed(client, auth_headers, db_engine) + draft_res = await client.post(f"/v1/social/place/{pid}/draft", headers=h, json={}) + assert draft_res.status_code == 200, draft_res.text + draft_post_id = draft_res.json()["post_id"] + + async with db_engine.begin() as c: + draft_job_type = ( + await c.execute( + text("SELECT job_type FROM jobs WHERE dedupe_key LIKE :k"), + {"k": f"social:%:{draft_post_id}"}, + ) + ).scalar_one() + assert HANDLERS[draft_job_type] is service.run_draft + + v2 = uuid.uuid4() + async with db_engine.begin() as c: + await c.execute( + text("UPDATE sites SET current_version_id=:v WHERE place_id=:p"), + {"v": v2, "p": pid}, + ) + post_id, token = await pending(db_engine, pid, uid, v2) + approve = await client.post( + f"/v1/social/approval/{post_id}/decision", json={"t": token, "approve": True} + ) + assert approve.json()["applied"] is True + + async with db_engine.begin() as c: + post_job_type = ( + await c.execute( + text("SELECT job_type FROM jobs WHERE dedupe_key LIKE :k"), + {"k": f"social:%:{post_id}"}, + ) + ).scalar_one() + assert HANDLERS[post_job_type] is service.run_post + + async def test_requires_fixed_domain(client, auth_headers, db_engine): h, pid, uid, v = await seed(client, auth_headers, db_engine) async with db_engine.begin() as c: @@ -120,7 +186,7 @@ async def test_prefetch_read_only_one_time_cas(client, auth_headers, db_engine): assert second.json()["applied"] is False async with db_engine.begin() as c: assert ( - await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=9")) + await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=10")) ).scalar_one() == 1 @@ -266,7 +332,7 @@ async def test_screen_approval_without_contract_never_queues_post( assert approved.json()["applied"] is True async with db_engine.begin() as c: assert ( - await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=9")) + await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=10")) ).scalar_one() == 0 @@ -588,7 +654,7 @@ async def test_publish_reused_text_inserts_approved_post_with_link_and_enqueues_ assert row.decided_via == "mini_blog" assert row.account_id == account_id job_count = ( - await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=9")) + await c.execute(text("SELECT count(*) FROM jobs WHERE job_type=10")) ).scalar_one() assert job_count == 1